diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml
new file mode 100644
index 0000000..4d207a5
--- /dev/null
+++ b/.github/workflows/tests.yml
@@ -0,0 +1,17 @@
+name: tests
+
+on:
+ push:
+ branches:
+ - main
+ pull_request:
+
+jobs:
+ tests:
+ name: Run Isolated Test Suite
+ runs-on: ubuntu-latest
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v4
+ - name: Run tests
+ run: bash scripts/tests.sh
diff --git a/.vscode/settings.json b/.vscode/settings.json
new file mode 100644
index 0000000..0fba2e0
--- /dev/null
+++ b/.vscode/settings.json
@@ -0,0 +1,18 @@
+{
+ "chat.tools.terminal.autoApprove": {
+ "/^bash scripts/tests\\.sh$/": {
+ "approve": true,
+ "matchCommandLine": true
+ },
+ "/^tmpdir=\\$\\(mktemp -d\\) && cd \"\\$tmpdir\" && git init >/dev/null && git config user\\.email test@example\\.com && git config user\\.name tester && git remote add origin git@github\\.com:example/test\\.git && mkdir C && echo init > README\\.md && git add README\\.md C && git commit -m init >/dev/null && echo one > C/file1 && git add C/file1 && git commit -m 'Merge pull request #101 from example/ops/C_1' >/dev/null && git tag C_1\\.0\\.0 && echo major > C/file2 && git add C/file2 && bash -x /home/alex/git/alexatkinson/github-action-gitops-autover/scripts/detectNewVersion\\.sh -d C -n C$/": {
+ "approve": true,
+ "matchCommandLine": true
+ }
+ },
+ "cSpell.words": [
+ "Envars",
+ "hefpndl",
+ "marvinpinto",
+ "vfpn"
+ ]
+}
\ No newline at end of file
diff --git a/README.md b/README.md
index 0fa02e5..333d489 100644
--- a/README.md
+++ b/README.md
@@ -12,7 +12,7 @@ Language/content agnostic method of automatically determining the [semantic vers
This is accomplished by counting the merges of branches matching the [naming scheme](#branch-naming-scheme) into the [main|master] branch. Folks familiar with Scrum/SAFe or GitFlow/fooFlow strategies will recognize this scheme.
-**Burning Questions**
+## Burning Questions
- **Yes** - This can indeed be implemented in repos that previously used different version increment methods.
- **Yes** - Jira will recognize the issue tag anywhere in the branch name -- it does not have to be a prefix for the integration to function.
@@ -22,6 +22,7 @@ This is accomplished by counting the merges of branches matching the [naming sch
## Recent Changes
+- 1.1.0: Squash-merge support (PR/MR classified via semver labels), GitLab label lookups, hard-fail on label lookup errors (592), and parallel full-history re-evaluation.
- 1.0.0: (non-breaking) Addition of support for mono-repos. IE: Discretely version specific directories.
- NOTE: Github, Jira, etc., were designed to host one product per repo/project. DO NOT create new mono-repo projects unless you're specifically tooling out to support them well.
- 0.3.1: Update the checkout action version to v4.
@@ -73,6 +74,16 @@ Note: Only required for setting up mono-repo versioning.
Eg: path/to/bob
Required: if mono-repo-mode: true
Default: ''
+
pr-label-overrides: [string]
+ Optional debug input for supplying PR label overrides manually.
+ Format: '123=semver:patch;124=semver:minor'
+ Required: false
+ Default: ''
+ force-re-evaluate: [bool]
+ Forces a re-evaluation of the entire git history.
+ Commit classification runs in parallel using nproc-1 jobs (minimum 1).
+ Required: false
+ Default: false
force-patch-increment: [bool]
Forces a PATCH increment if no other increment detected.
(Intended for development purposes only.)
@@ -123,6 +134,7 @@ To make use of the mono-repo support, simply add a block for the director you wi
uses: AlexAtkinson/github-action-gitops-autover@0.3.1
with:
mono-repo-product-name: bob
+ mono-repo-product-path: path/to/bob
This results in outputs like:
@@ -211,6 +223,33 @@ This action depends _only_ on the following _branch naming scheme_ being observe
For example, the name of the branch for a new awesome feature named Awesome Feature, might be: 'feature/awesome_feature'.
+### PR Label Precedence
+
+When a merged PR has one of the labels 'semver:patch', 'semver:minor', 'semver:major', or 'semver:breaking', that label takes precedence over the branch name for bump selection.
+
+Label lookup support by platform:
+
+
+ - GitHub
+ - Native label lookup. Requires GITHUB_TOKEN (or AUTOVER_GITHUB_TOKEN) and GITHUB_REPOSITORY.
+ - GitLab
+ - Native label lookup. Requires GITLAB_TOKEN (or AUTOVER_GITLAB_TOKEN) and CI_PROJECT_ID (or AUTOVER_GITLAB_PROJECT).
+ - Bitbucket
+ - Bitbucket Cloud has no PR labels. Use 'pr-label-overrides' or AUTOVER_PR_LABEL_OVERRIDES.
+
+
+If a label lookup is configured (credentials present) but cannot complete, the run fails with error 592 rather than silently falling back to branch-name classification.
+
+### Squash Merges
+
+Squash merges are supported where the PR/MR number can be recovered from the commit:
+
+- GitHub: squash commit subjects retain the PR number as a '(#123)' suffix.
+- Bitbucket: commit subjects use 'Merged in (pull request #123)'.
+- GitLab: the MR reference ('!123') is read from the commit subject or body.
+
+Since a squashed commit does not carry the source branch name, classification of squash merges relies on semver labels (or overrides). Unlabeled squash merges are ignored.
+
## Appropriate Use Cases
This action is _most_ suitable for git projects with the following operational design:
@@ -218,12 +257,14 @@ This action is _most_ suitable for git projects with the following operational d
- Each merge into main|master is intended to produce an artifact, following the "everything is potentially releasabe" approach.
This action is _not_ suitable for projects requiring:
- - pre-release, beta, etc., type fields. Such projects should depend upon their own language native tooling.
- - specific version numbers to be planned and orchestrated ahead of time (usually marketing efforts).
- - Exception: Major releases. These can be actioned on demand as outlined below.
- - rebase merges. Reminder: this action _depends_ on merge commit messages.
- - Exception: Patterns like: main < (merge-commit) < staging-branch < (rebase) work-branches
- - As long as main|master gets a merge commit message, everyone is happy.
+
+- pre-release, beta, etc., type fields. Such projects should depend upon their own language native tooling.
+- specific version numbers to be planned and orchestrated ahead of time (usually marketing efforts).
+ - Exception: Major releases. These can be actioned on demand as outlined below.
+- rebase merges. Reminder: this action _depends_ on merge or squash commit messages.
+ - Exception: Patterns like: main < (merge-commit) < staging-branch < (rebase) work-branches
+ - As long as main|master gets a merge commit message, everyone is happy.
+ - Exception: Squash merges where the PR carries a semver label (see Squash Merges above).
## Version Format
@@ -246,11 +287,13 @@ This increment can be accomplished in one of the following ways:
git tag 1.0.0
git push --tags
+In mono-repo mode, the MAJOR indicator must appear on a commit that touches the scoped directory, otherwise that product's history will not include it.
+
## Version Increment Logic
For those interested, here's some pseudo code:
- lastMajor = Extract from previous git tag (why option 1 is recommended)
+ lastMajor = Extract from previous git tag on the repo or scoped directory history
lastMinor = Extract from previous git tag
lastPatch = Extract from previous git tag
IF no previous git tag; THEN
@@ -261,6 +304,8 @@ For those interested, here's some pseudo code:
MAJOR = lastMajor + 1
MINOR = 0
PATCH = 0
+ ELSEIF merged PR has a semver label; THEN
+ use that label's bump instead of the branch name
ELSEIF merged feature/.* or enhancement/.* branches; THEN
MAJOR = lastMajor
MINOR = lastMinor + count of merged branches
@@ -278,6 +323,20 @@ For those interested, here's some pseudo code:
- When encountering this scenario, and a build is desired, you can simply create a branch with the appropriate naming convention and an empty commit, then merge it. Or use the bump scripts in the 'scripts/' directory of the repo for this action.
+- If a PR/MR label lookup is configured but fails (network, auth, etc.), the action fails with:
+
+ ERROR: 592 - PR/MR label lookup failed!
+
+ This is intentional: a version must never be computed from incomplete label data.
+
+## Local Testing
+
+Run the isolated test harness locally with:
+
+ bash scripts/tests.sh
+
+The script creates a temporary git repository, exercises repo and directory versioning paths, validates PR label precedence, squash-merge classification, and parallel full-history re-evaluation, and removes the fixture repo on exit. The same suite runs in CI on every pull request.
+
- Merged branches not conforming to the above naming scheme will simply be ignored.
- HINT: This can be useful when you don't want to increment the version.
- Align this with build 'on:push:branches:' workflow configuration to avoid unnecessary builds.
diff --git a/action.yml b/action.yml
index bedb03f..de2a484 100644
--- a/action.yml
+++ b/action.yml
@@ -21,6 +21,10 @@ inputs:
description: "The path to the product. IE: 'path/to/bob'. Required if 'mono-repo-mode' is enabled."
required: false
default: ''
+ pr-label-overrides:
+ description: "Optional/debug PR label overrides. Format: '123=semver:patch;124=semver:minor'."
+ required: false
+ default: ''
outputs:
new-version:
description: "New Version"
@@ -45,19 +49,23 @@ runs:
id: previous-version
run: |
cd $GITHUB_WORKSPACE
- echo "previous-version=$(${{ github.action_path }}/scripts/detectPreviousVersion.sh)" | tee $GITHUB_OUTPUT
+ opt=()
+ [[ "${{ inputs.mono-repo-product-name }}" != "" ]] && opt+=(-n "${{ inputs.mono-repo-product-name }}")
+ [[ "${{ inputs.mono-repo-product-path }}" != "" ]] && opt+=(-d "${{ inputs.mono-repo-product-path }}")
+ echo "previous-version=$(${{ github.action_path }}/scripts/detectPreviousVersion.sh "${opt[@]}")" | tee $GITHUB_OUTPUT
shell: bash
- name: Detect New Version
id: new-version
run: |
cd $GITHUB_WORKSPACE
- opt=''
- [[ "${{ github.event.inputs.force-re-evaluate }}" == 'true' ]] && opt='$opt -f'
- [[ "${{ github.event.inputs.force-patch-increment }}" == 'true' ]] && opt='$opt -p'
- [[ "${{ github.event.inputs.mono-repo-product-name }}" != "" ]] && opt='$opt -n ${{ github.event.inputs.mono-repo-product-name }}'
- [[ "${{ github.event.inputs.mono-repo-product-path }}" != "" ]] && opt='$opt -d ${{ github.event.inputs.mono-repo-product-path }}'
- new_version="$(${{ github.action_path }}/scripts/detectNewVersion.sh $opt)" || true
- debug_cmd="echo ${{ github.action_path }}/scripts/detectNewVersion.sh $opt" || true
+ opt=()
+ [[ "${{ inputs.force-re-evaluate }}" == 'true' ]] && opt+=(-f)
+ [[ "${{ inputs.force-patch-increment }}" == 'true' ]] && opt+=(-p)
+ [[ "${{ inputs.mono-repo-product-name }}" != "" ]] && opt+=(-n "${{ inputs.mono-repo-product-name }}")
+ [[ "${{ inputs.mono-repo-product-path }}" != "" ]] && opt+=(-d "${{ inputs.mono-repo-product-path }}")
+ [[ "${{ inputs.pr-label-overrides }}" != "" ]] && opt+=(-l "${{ inputs.pr-label-overrides }}")
+ new_version="$(${{ github.action_path }}/scripts/detectNewVersion.sh "${opt[@]}")" || true
+ debug_cmd="${{ github.action_path }}/scripts/detectNewVersion.sh $(printf '%q ' "${opt[@]}")" || true
echo "new-version=$new_version" | tee $GITHUB_OUTPUT
if [[ "$new_version" =~ "520" ]]; then
echo -e "ERROR: 520 - You must source this script when specifying an environment variable! Eg: '. ./foo.sh -e bar_ver'"
@@ -75,21 +83,28 @@ runs:
echo -e "ERROR: 591 - Unsupported origin host."
exit 1 || true
fi
+ if [[ "$new_version" =~ "592" ]]; then
+ echo -e "ERROR: 592 - PR/MR label lookup failed!"
+ exit 1 || true
+ fi
if [[ "$new_version" =~ "599" ]]; then
echo -e "ERROR: 599 - No feature, enhancement, fix, bugfix, hotfix, or ops branches detected!"
exit 1 || true
fi
shell: bash
+ env:
+ GITHUB_TOKEN: ${{ github.token }}
- name: Debug Cmd
id: debug-cmd
run: |
cd $GITHUB_WORKSPACE
- opt=''
- [[ "${{ github.event.inputs.force-re-evaluate }}" == 'true' ]] && opt='$opt -f'
- [[ "${{ github.event.inputs.force-patch-increment }}" == 'true' ]] && opt='$opt -p'
- [[ "${{ github.event.inputs.mono-repo-product-name }}" != "" ]] && opt='$opt -n ${{ github.event.inputs.mono-repo-product-name }}'
- [[ "${{ github.event.inputs.mono-repo-product-path }}" != "" ]] && opt='$opt -d ${{ github.event.inputs.mono-repo-product-path }}'
- debug_cmd="echo ${{ github.action_path }}/scripts/detectNewVersion.sh $opt" || true
+ opt=()
+ [[ "${{ inputs.force-re-evaluate }}" == 'true' ]] && opt+=(-f)
+ [[ "${{ inputs.force-patch-increment }}" == 'true' ]] && opt+=(-p)
+ [[ "${{ inputs.mono-repo-product-name }}" != "" ]] && opt+=(-n "${{ inputs.mono-repo-product-name }}")
+ [[ "${{ inputs.mono-repo-product-path }}" != "" ]] && opt+=(-d "${{ inputs.mono-repo-product-path }}")
+ [[ "${{ inputs.pr-label-overrides }}" != "" ]] && opt+=(-l "${{ inputs.pr-label-overrides }}")
+ debug_cmd="${{ github.action_path }}/scripts/detectNewVersion.sh $(printf '%q ' "${opt[@]}")" || true
echo "debug-cmd=$debug_cmd" | tee $GITHUB_OUTPUT
shell: bash
diff --git a/scripts/detectNewVersion.sh b/scripts/detectNewVersion.sh
index 7e28f9a..33d3187 100755
--- a/scripts/detectNewVersion.sh
+++ b/scripts/detectNewVersion.sh
@@ -29,7 +29,7 @@ NAME
${0##*/}
SYNOPSIS
- ${0##*/} [-hefpnd]
+ ${0##*/} [-hefpndl]
DESCRIPTION
Detects the new version for the repository by analyzing the gitflow branch history since the
@@ -43,6 +43,7 @@ DESCRIPTION
-e Export detected new version to defined variable, if valid.
-f Forces a re-evaluation of the entire git history.
+ Commit classification runs in parallel using nproc-1 jobs (minimum 1).
-p Increments PATCH version on _every_ run.
WARN: This is intended development use only.
@@ -53,6 +54,9 @@ DESCRIPTION
-d The directory of the product to version. EG: 'path/to/bob'.
+ -l Local/debug label overrides for merged PRs.
+ Format: '123=semver:patch;124=semver:minor'.
+
EXAMPLES
The following detects the new version for the repo.
@@ -81,7 +85,7 @@ fi
# --------------------------------------------------------------------------------------------------
OPTIND=1
-while getopts "he:vfpn:d:" opt; do
+while getopts "he:vfpn:d:l:" opt; do
case $opt in
h)
printHelp
@@ -112,6 +116,9 @@ while getopts "he:vfpn:d:" opt; do
arg_d_opt="--full-history"
arg_opts="$arg_opts -d $OPTARG"
;;
+ l)
+ arg_l_val="$OPTARG"
+ ;;
*)
echo -e "\e[01;31mERROR\e[00m: 570 - Invalid argument!"
printHelp
@@ -129,7 +136,6 @@ shift $((OPTIND-1))
# Variables
# --------------------------------------------------------------------------------------------------
-IFS_BAK=$IFS
tsCmd='date --utc +%FT%T.%3NZ'
relative_path="$(dirname "${BASH_SOURCE[0]}")"
@@ -156,18 +162,12 @@ origin=$(git config --get remote.origin.url)
case "$origin_host" in
github)
merge_string="Merge pull request #"
- column=7
- field=2
;;
gitlab)
merge_string="Merge branch"
- column=4
- field=1
;;
bitbucket)
merge_string="Merged in"
- column=4
- field=1
;;
*)
echo -e "\e[01;31mERROR\e[0m: 591 - Unsupported origin host."
@@ -175,6 +175,192 @@ case "$origin_host" in
;;
esac
+labelOverrideSource="${arg_l_val:-${AUTOVER_PR_LABEL_OVERRIDES:-}}"
+
+extract_pr_number() {
+ local subject="$1"
+ local commit_hash="$2"
+ local number=''
+
+ case "$origin_host" in
+ github)
+ number=$(sed -n 's/^Merge pull request #\([0-9][0-9]*\).*/\1/p' <<< "$subject")
+ # Squash merges retain the PR number as a '(#123)' suffix on the commit subject.
+ [[ -z "$number" ]] && number=$(sed -n 's/.*(#\([0-9][0-9]*\))$/\1/p' <<< "$subject")
+ ;;
+ gitlab)
+ number=$(grep -o '![0-9][0-9]*' <<< "$subject" | head -n 1 | tr -d '!')
+ # Merge commits carry the MR reference in the body: 'See merge request group/project!123'.
+ [[ -z "$number" && -n "$commit_hash" ]] && number=$(git show -s --format=%b "$commit_hash" 2>/dev/null | grep -o '![0-9][0-9]*' | head -n 1 | tr -d '!')
+ ;;
+ bitbucket)
+ # Both merge and squash commits use: 'Merged in (pull request #123)'.
+ number=$(sed -n 's/.*(pull request #\([0-9][0-9]*\)).*/\1/p' <<< "$subject")
+ ;;
+ esac
+
+ echo "$number"
+}
+
+get_override_labels() {
+ local pr_number="$1"
+ [[ -z "$labelOverrideSource" ]] && return 0
+
+ while IFS= read -r mapping; do
+ [[ -z "$mapping" ]] && continue
+ local mapping_pr="${mapping%%=*}"
+ if [[ "$mapping_pr" == "$pr_number" && "$mapping" == *"="* ]]; then
+ echo "${mapping#*=}"
+ return 0
+ fi
+ done < <(tr ';' '\n' <<< "$labelOverrideSource")
+}
+
+get_github_labels() {
+ local pr_number="$1"
+
+ local github_token="${GITHUB_TOKEN:-${AUTOVER_GITHUB_TOKEN:-}}"
+ local github_repo="${GITHUB_REPOSITORY:-${AUTOVER_GITHUB_REPOSITORY:-}}"
+ [[ -z "$github_token" || -z "$github_repo" ]] && return 0
+
+ local api_root="${GITHUB_API_URL:-https://api.github.com}"
+ local response
+ if ! response=$(curl -fsSL \
+ -H "Authorization: Bearer ${github_token}" \
+ -H 'Accept: application/vnd.github+json' \
+ "${api_root}/repos/${github_repo}/issues/${pr_number}/labels" 2>/dev/null); then
+ echo -e "\e[01;31mERROR\e[00m: 592 - Label lookup failed for PR #${pr_number}!" >&2
+ return 1
+ fi
+
+ grep -o '"name"[[:space:]]*:[[:space:]]*"[^"]*"' <<< "$(tr -d '\n' <<< "$response")" | sed 's/.*"name"[[:space:]]*:[[:space:]]*"\([^"]*\)"/\1/' | paste -sd ',' -
+}
+
+get_gitlab_labels() {
+ local mr_number="$1"
+
+ local gitlab_token="${GITLAB_TOKEN:-${AUTOVER_GITLAB_TOKEN:-}}"
+ local gitlab_project="${CI_PROJECT_ID:-${AUTOVER_GITLAB_PROJECT:-}}"
+ [[ -z "$gitlab_token" || -z "$gitlab_project" ]] && return 0
+
+ local api_root="${CI_API_V4_URL:-https://gitlab.com/api/v4}"
+ local response
+ if ! response=$(curl -fsSL \
+ -H "PRIVATE-TOKEN: ${gitlab_token}" \
+ "${api_root}/projects/${gitlab_project}/merge_requests/${mr_number}" 2>/dev/null); then
+ echo -e "\e[01;31mERROR\e[00m: 592 - Label lookup failed for MR !${mr_number}!" >&2
+ return 1
+ fi
+
+ tr -d '\n' <<< "$response" | sed -n 's/.*"labels":\[\([^]]*\)\].*/\1/p' | tr ',' '\n' | tr -d '"' | paste -sd ',' -
+}
+
+get_pr_labels() {
+ local pr_number="$1"
+ local labels
+
+ labels=$(get_override_labels "$pr_number")
+ if [[ -n "$labels" ]]; then
+ echo "$labels"
+ return 0
+ fi
+
+ case "$origin_host" in
+ github)
+ get_github_labels "$pr_number"
+ ;;
+ gitlab)
+ get_gitlab_labels "$pr_number"
+ ;;
+ bitbucket)
+ # Bitbucket Cloud has no PR labels. Overrides (-l/AUTOVER_PR_LABEL_OVERRIDES) still apply.
+ true
+ ;;
+ esac
+}
+
+detect_increment_from_labels() {
+ local labels="$1"
+ [[ -z "$labels" ]] && return 0
+
+ local normalized_labels
+ normalized_labels=",$(tr '[:upper:]' '[:lower:]' <<< "$labels" | tr -d ' '),"
+
+ [[ "$normalized_labels" == *",semver:major,"* || "$normalized_labels" == *",semver-major,"* || "$normalized_labels" == *",semver:breaking,"* || "$normalized_labels" == *",semver-breaking,"* ]] && {
+ echo 'major'
+ return 0
+ }
+ [[ "$normalized_labels" == *",semver:minor,"* || "$normalized_labels" == *",semver-minor,"* ]] && {
+ echo 'minor'
+ return 0
+ }
+ [[ "$normalized_labels" == *",semver:patch,"* || "$normalized_labels" == *",semver-patch,"* ]] && {
+ echo 'patch'
+ return 0
+ }
+}
+
+detect_increment_from_branch() {
+ local subject="$1"
+ local branch_ref=''
+ local branch_type=''
+
+ case "$origin_host" in
+ github)
+ [[ "$subject" != "$merge_string"* ]] && return 0
+ branch_ref="${subject#* from }"
+ branch_ref="${branch_ref%% *}"
+ branch_type="${branch_ref#*/}"
+ branch_type="${branch_type%%/*}"
+ ;;
+ gitlab)
+ branch_ref=$(sed -n "s/^Merge branch '\([^']*\)'.*/\1/p" <<< "$subject")
+ branch_type="${branch_ref%%/*}"
+ ;;
+ bitbucket)
+ branch_ref="${subject#Merged in }"
+ branch_ref="${branch_ref%% *}"
+ branch_type="${branch_ref%%/*}"
+ ;;
+ esac
+
+ branch_type="${branch_type,,}"
+ case "$branch_type" in
+ feature|enhancement)
+ echo 'minor'
+ ;;
+ fix|bugfix|hotfix|ops)
+ echo 'patch'
+ ;;
+ esac
+}
+
+classify_commit() {
+ local historyLine="$1"
+ local commitHash="${historyLine%%$'\t'*}"
+ local commitSubject="${historyLine#*$'\t'}"
+ local incrementType=''
+ local prNumber=''
+ local prLabels=''
+
+ if grep -q '+semver' <<< "$commitSubject" && grep -qi 'major\|breaking' <<< "$commitSubject"; then
+ echo 'major'
+ return 0
+ fi
+
+ prNumber=$(extract_pr_number "$commitSubject" "$commitHash")
+ if [[ -n "$prNumber" ]]; then
+ if ! prLabels=$(get_pr_labels "$prNumber"); then
+ return 1
+ fi
+ incrementType=$(detect_increment_from_labels "$prLabels")
+ fi
+
+ [[ -z "$incrementType" ]] && incrementType=$(detect_increment_from_branch "$commitSubject")
+
+ echo "$incrementType"
+}
+
# --------------------------------------------------------------------------------------------------
# Sanity (2/2)
# --------------------------------------------------------------------------------------------------
@@ -186,44 +372,60 @@ if [[ -n $arg_e ]]; then
fi
fi
-git log $arg_d_opt --pretty=oneline "$lastVersionCommitHash".."$lastCommitHash" $arg_d_val | grep '+semver' | grep -q 'major\|breaking' && incrementMajor='true'
-
-if [[ $incrementMajor != 'true' ]]; then
- IFS=$'\r\n'
- if [[ -n $arg_f ]]; then
- for i in $(git log $arg_d_opt --pretty=oneline "${firstCommitHash}".."${lastCommitHash}" $arg_d_val | awk -v s="$merge_string" -v c="$column" '$0 ~ s {print $c}' | awk -v f="$field" -F'/' '{print $f}' | tr -d "'" | grep -i '^enhancement$\|^feature$\|^fix$\|^hotfix$\|^bugfix$\|^ops$' | awk -F '\r' '{print $1}' | sort | uniq -c | sort -nr) ; do
- varname=$(echo "$i" | awk '{print $2}')
- varname=${varname,,}
- value=$(echo "$i" | awk '{print $1}')
- value=${value,,}
- declare count_"$varname"="$value"
- done
- else
- for i in $(git log $arg_d_opt --pretty=oneline "${lastVersionCommitHash}".."${lastCommitHash}" $arg_d_val | awk -v s="$merge_string" -v c="$column" '$0 ~ s {print $c}' | awk -v f="$field" -F'/' '{print $f}' | tr -d "'" | grep -i '^enhancement$\|^feature$\|^fix$\|^hotfix$\|^bugfix$\|^ops$' | awk -F '\r' '{print $1}' | sort | uniq -c | sort -nr) ; do
- varname=$(echo "$i" | awk '{print $2}')
- varname=${varname,,}
- value=$(echo "$i" | awk '{print $1}')
- value=${value,,}
- declare count_"$varname"="$value"
- done
- fi
- IFS=$IFS_BAK
-fi
+historyStartCommit="$lastVersionCommitHash"
+[[ -n $arg_f ]] && historyStartCommit="$firstCommitHash"
+
+historyCmd=(git log)
+[[ -n $arg_d_opt ]] && historyCmd+=("$arg_d_opt")
+historyCmd+=(--pretty=format:%H%x09%s "${historyStartCommit}..${lastCommitHash}")
+[[ -n $arg_d ]] && historyCmd+=(-- "$arg_d_val")
-# echo "enh: $count_enhancement"
-# echo "feat: $count_feature"
-# echo "fix: $count_fix"
-# echo "bug: $count_bugfix"
-# echo "hot: $count_hotfix"
-# echo "ops: $count_ops"
+minorIncrementCount=0
+patchIncrementCount=0
if [[ -n $arg_f ]]; then
- true
-else
- if [[ -z $incrementMajor && -z $count_feature && -z $count_enhancement && -z $count_fix && -z $count_bugfix && -z $count_hotfix && -z $count_ops ]]; then
- echo -e "\e[01;31mERROR\e[00m: 599 - No feature, enhancement, fix, bugfix, hotfix, or ops branches detected!"
+ # Full re-evaluation can walk very large histories; classify commits in parallel.
+ parallelJobs=$(( $(nproc 2>/dev/null || echo 2) - 1 ))
+ (( parallelJobs < 1 )) && parallelJobs=1
+
+ export -f classify_commit extract_pr_number get_pr_labels get_override_labels get_github_labels get_gitlab_labels detect_increment_from_labels detect_increment_from_branch
+ export origin_host merge_string labelOverrideSource
+
+ if ! classificationResults=$("${historyCmd[@]}" | xargs -r -d '\n' -P "$parallelJobs" -n 1 bash -c 'classify_commit "$1"' _); then
+ echo -e "\e[01;31mERROR\e[00m: 592 - Label lookup failed!"
exit 1
fi
+
+ grep -qx 'major' <<< "$classificationResults" && incrementMajor='true'
+ minorIncrementCount=$(grep -cx 'minor' <<< "$classificationResults")
+ patchIncrementCount=$(grep -cx 'patch' <<< "$classificationResults")
+else
+ while IFS= read -r historyLine || [[ -n "$historyLine" ]]; do
+ [[ -z "$historyLine" ]] && continue
+
+ if ! incrementType=$(classify_commit "$historyLine"); then
+ echo -e "\e[01;31mERROR\e[00m: 592 - Label lookup failed!"
+ exit 1
+ fi
+
+ case "$incrementType" in
+ major)
+ incrementMajor='true'
+ break
+ ;;
+ minor)
+ minorIncrementCount=$((minorIncrementCount + 1))
+ ;;
+ patch)
+ patchIncrementCount=$((patchIncrementCount + 1))
+ ;;
+ esac
+ done < <("${historyCmd[@]}")
+fi
+
+if [[ -z $arg_f && $incrementMajor != 'true' && $minorIncrementCount -eq 0 && $patchIncrementCount -eq 0 ]]; then
+ echo -e "\e[01;31mERROR\e[00m: 599 - No feature, enhancement, fix, bugfix, hotfix, or ops branches detected!"
+ exit 1
fi
# --------------------------------------------------------------------------------------------------
@@ -234,19 +436,14 @@ if [[ $incrementMajor == 'true' ]]; then
newVersionMajor=$((lastVersionMajor + 1))
newVersionMinor='0'
newVersionPatch='0'
-elif [[ -n $count_feature || -n $count_enhancement ]]; then
+elif [[ $minorIncrementCount -gt 0 ]]; then
newVersionMajor=$lastVersionMajor
- [[ -n $count_feature ]] && newVersionMinor=$((lastVersionMinor + count_feature))
- [[ -n $count_enhancement ]] && newVersionMinor=$((newVersionMinor + count_enhancement))
+ newVersionMinor=$((lastVersionMinor + minorIncrementCount))
newVersionPatch='0'
-elif [[ -n $count_fix || -n $count_bugfix || -n $count_hotfix || -n $count_ops ]]; then
+elif [[ $patchIncrementCount -gt 0 ]]; then
newVersionMajor=$lastVersionMajor
newVersionMinor=$lastVersionMinor
- newVersionPatch=$lastVersionPatch
- [[ -n $count_fix ]] && newVersionPatch=$((lastVersionPatch + count_fix))
- [[ -n $count_bugfix ]] && newVersionPatch=$((newVersionPatch + count_bugfix))
- [[ -n $count_hotfix ]] && newVersionPatch=$((newVersionPatch + count_hotfix))
- [[ -n $count_ops ]] && newVersionPatch=$((newVersionPatch + count_ops))
+ newVersionPatch=$((lastVersionPatch + patchIncrementCount))
elif [[ -n $arg_p ]]; then
newVersionMajor=$lastVersionMajor
newVersionMinor=$lastVersionMinor
diff --git a/scripts/detectPreviousVersion.sh b/scripts/detectPreviousVersion.sh
index 7884b5f..402cf92 100755
--- a/scripts/detectPreviousVersion.sh
+++ b/scripts/detectPreviousVersion.sh
@@ -88,7 +88,6 @@ while getopts "hv9cn:d:" opt; do
d)
arg_d='set'
arg_d_val="$OPTARG"
- arg_d_opt="--full-history"
arg_opts="$arg_opts -d $OPTARG"
;;
*)
@@ -117,9 +116,22 @@ fi
relative_path="$(dirname "${BASH_SOURCE[0]}")"
dir="$(realpath "${relative_path}")"
-lastVersion=$(git for-each-ref --sort=creatordate --format '%(refname:lstrip=2)' refs/tags | grep -E "$semverRegex" | tail -n 1)
-# Support mono-repos where a product name is specified.
-[[ -n $arg_n ]] && lastVersion=$(git for-each-ref --sort=creatordate --format '%(refname:lstrip=2)' refs/tags | grep "$arg_n_val" | grep -E "$semverRegex" | tail -n 1)
+historyCmd=(git log --format='%H')
+[[ -n $arg_d ]] && historyCmd+=(--full-history)
+historyCmd+=(HEAD)
+[[ -n $arg_d ]] && historyCmd+=(-- "$arg_d_val")
+
+while IFS= read -r historyHash || [[ -n "$historyHash" ]]; do
+ [[ -z "$historyHash" ]] && continue
+
+ candidateTags=$(git tag --points-at "$historyHash" | grep -E "$semverRegex")
+ [[ -n $arg_n ]] && candidateTags=$(printf '%s\n' "$candidateTags" | grep -F "$arg_n_val")
+
+ if [[ -n "$candidateTags" ]]; then
+ lastVersion=$(printf '%s\n' "$candidateTags" | sort -V | tail -n 1)
+ break
+ fi
+done < <("${historyCmd[@]}")
# --------------------------------------------------------------------------------------------------
# Sanity (2/2)
diff --git a/scripts/tests.sh b/scripts/tests.sh
index 8aafcb2..2e8298a 100755
--- a/scripts/tests.sh
+++ b/scripts/tests.sh
@@ -1,164 +1,347 @@
#!/usr/bin/env bash
-# test.sh
-
-TEST="test_$(date --utc +"%s")"
-trap 'rm -rf $TEST; \
- git reset --hard $STARTING_COMMIT;' EXIT
- #git reset HEAD~$TEST_COMMIT_COUNT;' EXIT
-
-STARTING_COMMIT=$(git rev-parse HEAD)
-
-TEST_COMMIT_COUNT=0
-
-function printHeading () {
- txt="$@";
- printf "\n\e[01;39m${txt}\e[0m ";
- printf '\n%*s' "$((${COLUMNS}-$((${COLUMNS}-$(wc -c<<<$txt)+1))))" | tr ' ' -;
- printf '\n'
-}
-
-function add_history() {
- if [[ $# -ne 3 ]] && [[ $# -ne 4 ]]; then echo "ERROR: Exactly 3 or 4 arguments required!"; return 1; fi
- DIR="$1"
- COUNT="$2"
- BRANCH_TYPE="$3"
- MAJOR="$4"
- cd "$DIR"
- for i in $(seq 1 $COUNT); do
- TEST_FILE="${i}_$(date +"%s%6N")"
- touch $TEST_FILE
- git add "$TEST_FILE" >/dev/null 2>&1
- [[ -n $MAJOR ]] && git commit -m "+semver major $TEST" >/dev/null 2>&1
- git commit -m "Merge pull request #9999 from AlexAtkinson/$BRANCH_TYPE/${TEST}_$i" >/dev/null 2>&1
+
+TEST_ROOT=$(mktemp -d)
+TEST_REPO="$TEST_ROOT/repo"
+PR_NUMBER=1000
+LABEL_OVERRIDES=''
+FAILURE=''
+
+trap 'rm -rf "$TEST_ROOT"' EXIT
+
+printHeading() {
+ local txt="$*"
+ printf "\n\e[01;39m%s\e[0m\n" "$txt"
+ printf '%*s\n' "${#txt}" '' | tr ' ' '-'
+}
+
+script_dir="$(realpath "$(dirname "${BASH_SOURCE[0]}")")"
+
+setup_repo() {
+ mkdir -p "$TEST_REPO"
+ cd "$TEST_REPO" || exit 1
+ git init >/dev/null 2>&1
+ git config user.name test-user
+ git config user.email test@example.com
+ git remote add origin git@github.com:example/test.git
+ mkdir -p A B C D E F
+ echo init > README.md
+ git add README.md A B C >/dev/null 2>&1
+ git commit -m init >/dev/null 2>&1
+}
+
+append_label_override() {
+ local pr_number="$1"
+ local label="$2"
+ [[ -z "$label" ]] && return 0
+
+ [[ -n "$LABEL_OVERRIDES" ]] && LABEL_OVERRIDES+=";"
+ LABEL_OVERRIDES+="${pr_number}=${label}"
+}
+
+add_history() {
+ if [[ $# -lt 3 || $# -gt 4 ]]; then
+ echo "ERROR: add_history requires 3 or 4 arguments"
+ exit 1
+ fi
+
+ local directory="$1"
+ local count="$2"
+ local branch_type="$3"
+ local label_override="$4"
+ local pr_number=''
+ local test_file=''
+ local i=''
+
+ for i in $(seq 1 "$count"); do
+ PR_NUMBER=$((PR_NUMBER + 1))
+ pr_number="$PR_NUMBER"
+ test_file="$directory/${branch_type}_${i}_$(date +%s%N)"
+ echo "$pr_number" >> "$test_file"
+ git add "$test_file" >/dev/null 2>&1
+ git commit -m "Merge pull request #${pr_number} from example/${branch_type}/${directory}_${i}" >/dev/null 2>&1
+ append_label_override "$pr_number" "$label_override"
+ done
+}
+
+add_repo_history() {
+ if [[ $# -lt 2 || $# -gt 3 ]]; then
+ echo "ERROR: add_repo_history requires 2 or 3 arguments"
+ exit 1
+ fi
+
+ local count="$1"
+ local branch_type="$2"
+ local label_override="$3"
+ local pr_number=''
+ local test_file=''
+ local i=''
+
+ for i in $(seq 1 "$count"); do
+ PR_NUMBER=$((PR_NUMBER + 1))
+ pr_number="$PR_NUMBER"
+ test_file="repo_${branch_type}_${i}_$(date +%s%N)"
+ echo "$pr_number" >> "$test_file"
+ git add "$test_file" >/dev/null 2>&1
+ git commit -m "Merge pull request #${pr_number} from example/${branch_type}/repo_${i}" >/dev/null 2>&1
+ append_label_override "$pr_number" "$label_override"
done
- cd - >/dev/null 2>&1
-}
-
-function test_previous() {
- if [[ $# -lt 3 ]]; then echo "ERROR: At least 3 arguments required!"; return 1; fi
- TEST_TYPE="${1:-Repository}" # Repository, Directory
- DIRECTORY="${2:-./}"
- ASSERTION="$3"
- COMMENT="${@:4}"; [[ -z $COMMENT ]] && COMMENT="No Comment"
- if [[ "$TEST_TYPE" == "Repository" ]]; then
- echo -e "\e[01;39m$TEST_TYPE: Previous Version ($COMMENT)\e[0m"
- TEST_OUTPUT=$(scripts/detectPreviousVersion.sh)
- if grep -q "$ASSERTION" <<<$TEST_OUTPUT; then RESULT="\e[01;32mOK\e[0m"; else RESULT="\e[01;31mFAIL\e[0m"; FAILURE="TRUE"; fi
- echo -e " $RESULT - $TEST_OUTPUT"
+}
+
+add_repo_major_commit() {
+ local marker_file=''
+
+ marker_file="repo_major_$(date +%s%N)"
+ echo major > "$marker_file"
+ git add "$marker_file" >/dev/null 2>&1
+ git commit -m "+semver major tests" >/dev/null 2>&1
+}
+
+add_repo_squash_history() {
+ if [[ $# -lt 1 || $# -gt 2 ]]; then
+ echo "ERROR: add_repo_squash_history requires 1 or 2 arguments"
+ exit 1
fi
- if [[ "$TEST_TYPE" == "Directory" ]]; then
- echo -e "\e[01;39m$TEST_TYPE - $DIRECTORY: Previous Version ($COMMENT)\e[0m"
- TEST_OUTPUT=$(scripts/detectPreviousVersion.sh -d "$DIRECTORY" -n "${DIRECTORY##*/}")
- if grep -q "$ASSERTION" <<<$TEST_OUTPUT; then RESULT="\e[01;32mOK\e[0m"; else RESULT="\e[01;31mFAIL\e[0m"; FAILURE="TRUE"; fi
- echo -e " $RESULT - $TEST_OUTPUT"
+
+ local count="$1"
+ local label_override="$2"
+ local pr_number=''
+ local test_file=''
+ local i=''
+
+ for i in $(seq 1 "$count"); do
+ PR_NUMBER=$((PR_NUMBER + 1))
+ pr_number="$PR_NUMBER"
+ test_file="repo_squash_${i}_$(date +%s%N)"
+ echo "$pr_number" >> "$test_file"
+ git add "$test_file" >/dev/null 2>&1
+ git commit -m "Some squashed change ${i} (#${pr_number})" >/dev/null 2>&1
+ append_label_override "$pr_number" "$label_override"
+ done
+}
+
+add_major_commit() {
+ local directory="$1"
+ local marker_file=''
+
+ marker_file="$directory/major_$(date +%s%N)"
+ echo major > "$marker_file"
+ git add "$marker_file" >/dev/null 2>&1
+ git commit -m "+semver major tests" >/dev/null 2>&1
+}
+
+run_detect_previous() {
+ local directory="$1"
+
+ if [[ "$directory" == './' ]]; then
+ "$script_dir/detectPreviousVersion.sh"
+ else
+ "$script_dir/detectPreviousVersion.sh" -d "$directory" -n "${directory##*/}"
fi
}
-function test_new() {
- if [[ $# -lt 3 ]]; then echo "ERROR: At least 3 arguments required!"; return 1; fi
- TEST_TYPE="${1:-Repository}" # Repository, Directory
- DIRECTORY="${2:-./}"
- ASSERTION="$3"
- COMMENT="${@:4}"; [[ -z $COMMENT ]] && COMMENT="No Comment"
- if [[ "$TEST_TYPE" == "Repository" ]]; then
- echo -e "\e[01;39m$TEST_TYPE: New Version ($COMMENT)\e[0m"
- TEST_OUTPUT=$(scripts/detectNewVersion.sh)
- if grep -q "$ASSERTION" <<<$TEST_OUTPUT; then RESULT="\e[01;32mOK\e[0m"; else RESULT="\e[01;31mFAIL\e[0m"; FAILURE="TRUE"; fi
- echo -e " $RESULT - $TEST_OUTPUT"
+run_detect_new() {
+ local directory="$1"
+ local cmd=("$script_dir/detectNewVersion.sh")
+
+ if [[ "$directory" != './' ]]; then
+ cmd+=(-d "$directory" -n "${directory##*/}")
fi
- if [[ "$TEST_TYPE" == "Directory" ]]; then
- echo -e "\e[01;39m$TEST_TYPE - $DIRECTORY: New Version ($COMMENT)\e[0m"
- TEST_OUTPUT=$(scripts/detectNewVersion.sh -d "$DIRECTORY" -n "${DIRECTORY##*/}")
- if grep -q "$ASSERTION" <<<$TEST_OUTPUT; then RESULT="\e[01;32mOK\e[0m"; else RESULT="\e[01;31mFAIL\e[0m"; FAILURE="TRUE"; fi
- echo -e " $RESULT - $TEST_OUTPUT"
+
+ [[ -n "$LABEL_OVERRIDES" ]] && cmd+=(-l "$LABEL_OVERRIDES")
+
+ "${cmd[@]}" 2>&1
+}
+
+assert_match() {
+ local label="$1"
+ local expected="$2"
+ local actual="$3"
+
+ if grep -q "$expected" <<< "$actual"; then
+ echo -e " \e[01;32mOK\e[0m - $actual"
+ summary_row "$label" "✅" "$actual"
+ else
+ FAILURE='true'
+ echo -e " \e[01;31mFAIL\e[0m - expected '$expected', got '$actual'"
+ summary_row "$label" "❌" "expected '$expected', got '$actual'"
fi
}
-relative_path="$(dirname "${BASH_SOURCE[0]}")"
-dir="$(realpath "${relative_path}")"
-
-ci_name=$("${dir}/detect-ci.sh")
-origin=$(git config --get remote.origin.url)
-
-#origin=${ci_name:-origin}
-# Executes in ANY CI so long as repo origin is one of the following.
-# Uncomment origin override to restrict this.
-[[ "$origin" =~ "git@github.com"* || "$ci_name" == "github" ]] && origin_host=github
-[[ "$origin" =~ "git@gitlab.com"* || "$ci_name" == "gitlab" ]] && origin_host=gitlab
-[[ "$origin" =~ "git@bitbucket.com"* || "$ci_name" == "bitbucket" ]] && origin_host=bitbucket
-
-case "$origin_host" in
- github)
- merge_string="Merge pull request #"
- column=7
- field=2
- ;;
- gitlab)
- merge_string="Merge branch"
- column=4
- field=1
- ;;
- bitbucket)
- merge_string="Merged in"
- column=4
- field=1
- ;;
- *)
- echo -e "\e[01;31mERROR\e[0m: 591 - Unsupported origin host."
- exit 1
- ;;
-esac
-
-
-# TEST: Repo Versioning
-
-## Activities
-
-# Tests:
-# - Repo Versioning
-# - Diectory Versioning
-# - x Minor Increments
-# - x Patch Increments
-
-printHeading Running Test: $TEST
-
-echo NOTE: These tests are not committed.
-
-mkdir -p $TEST/{A..C}
-
-# Directory Test: A
-# ASSERTIONS:
-# - Previous version is: A_0.0.0
-# - New Version is: ERROR: 599
-test_previous "Repository" "./" "$(scripts/detectPreviousVersion.sh)"
-test_new "Repository" "./" " 599"
-
-test_previous "Directory" "$TEST/A" "A_0.0.0"
-test_new "Directory" "$TEST/A" " 599"
-
-test_previous "Directory" "$TEST/B" "B_0.0.0"
-add_history "$TEST/B" 3 ops
-test_new "Directory" "$TEST/B" "B_0.0.3" patches +3
-git tag -a "B_0.0.3" -m "TAG"
-test_previous "Directory" "$TEST/B" "B_0.0.3" previous version tagged
-add_history "$TEST/B" 17 ops
-test_new "Directory" "$TEST/B" "B_0.0.20" patches +17
-git tag -d "B_0.0.3" >/dev/null 2>&1
-
-test_previous "Directory" "$TEST/C" "C_0.0.0"
-add_history "$TEST/C" 9 ops
-test_new "Directory" "$TEST/C" "C_0.0.9" patches +9
-git tag -a "C_0.0.9" -m "TAG"
-test_previous "Directory" "$TEST/C" "C_0.0.9" previous version tagged
-test_new "Directory" "$TEST/C" " 599"
-
-add_history "$TEST/C" 5 feature
-test_new "Directory" "$TEST/C" "C_0.5.0" features +5
-git tag -a "C_0.5.0" -m "TAG"
-add_history "$TEST/C" 19 ops
-test_new "Directory" "$TEST/C" "C_0.5.19" patches +19
-add_history "$TEST/C" 1 features major
-test_new "Directory" "$TEST/C" "C_1.0.0" features +1 BREAKING
-git tag -d "C_0.0.9" >/dev/null 2>&1
-git tag -d "C_0.5.0" >/dev/null 2>&1
+summary_init() {
+ [[ -z "$GITHUB_STEP_SUMMARY" ]] && return 0
+
+ {
+ echo '### Test Results'
+ echo ''
+ echo '| Test | Result | Output |'
+ echo '| ---- | :----: | ------ |'
+ } >> "$GITHUB_STEP_SUMMARY"
+}
+
+summary_row() {
+ [[ -z "$GITHUB_STEP_SUMMARY" ]] && return 0
+
+ local label="$1"
+ local result="$2"
+ local output="$3"
+
+ # Strip ANSI color codes and escape pipes so the output renders cleanly in markdown.
+ output=$(sed -e 's/\x1b\[[0-9;]*m//g' -e 's/|/\\|/g' <<< "$output")
+ echo "| ${label} | ${result} | ${output} |" >> "$GITHUB_STEP_SUMMARY"
+}
+
+test_previous() {
+ local directory="$1"
+ local expected="$2"
+ local comment="$3"
+ local actual=''
+ local label="Previous Version ${directory} (${comment})"
+
+ echo -e "\e[01;39m${label}\e[0m"
+ actual=$(run_detect_previous "$directory")
+ assert_match "$label" "$expected" "$actual"
+}
+
+test_new() {
+ local directory="$1"
+ local expected="$2"
+ local comment="$3"
+ local actual=''
+ local label="New Version ${directory} (${comment})"
+
+ echo -e "\e[01;39m${label}\e[0m"
+ actual=$(run_detect_new "$directory")
+ assert_match "$label" "$expected" "$actual"
+}
+
+test_new_full() {
+ local directory="$1"
+ local expected="$2"
+ local comment="$3"
+ local actual=''
+ local label="New Version (full re-evaluation) ${directory} (${comment})"
+ local cmd=("$script_dir/detectNewVersion.sh" -f)
+
+ if [[ "$directory" != './' ]]; then
+ cmd+=(-d "$directory" -n "${directory##*/}")
+ fi
+
+ [[ -n "$LABEL_OVERRIDES" ]] && cmd+=(-l "$LABEL_OVERRIDES")
+
+ echo -e "\e[01;39m${label}\e[0m"
+ actual=$("${cmd[@]}" 2>&1)
+ assert_match "$label" "$expected" "$actual"
+}
+
+setup_repo
+
+printHeading 'Running isolated local tests'
+summary_init
+
+test_previous './' '0.0.0' 'repo initializes at zero'
+test_new './' '599' 'repo without qualifying merges'
+
+add_repo_history 2 feature
+add_repo_history 1 enhancement
+test_new './' '0.3.0' 'repo feature and enhancement branches increment minor versions'
+git tag -a '0.3.0' -m TAG >/dev/null 2>&1
+test_previous './' '0.3.0' 'repo previous version tagged'
+
+add_repo_history 1 fix
+add_repo_history 1 bugfix
+add_repo_history 1 hotfix
+add_repo_history 1 ops
+test_new './' '0.3.4' 'repo fix, bugfix, hotfix, and ops branches increment patch versions'
+git tag -a '0.3.4' -m TAG >/dev/null 2>&1
+test_previous './' '0.3.4' 'repo previous patch version tagged'
+
+add_repo_history 1 feature 'semver:patch'
+test_new './' '0.3.5' 'repo semver patch label overrides feature branch'
+git tag -a '0.3.5' -m TAG >/dev/null 2>&1
+
+add_repo_history 1 fix 'semver:minor'
+test_new './' '0.4.0' 'repo semver minor label overrides fix branch'
+git tag -a '0.4.0' -m TAG >/dev/null 2>&1
+
+add_repo_history 1 ops 'semver:major'
+test_new './' '1.0.0' 'repo semver major label overrides ops branch'
+git tag -a '1.0.0' -m TAG >/dev/null 2>&1
+
+add_repo_major_commit
+test_new './' '2.0.0' 'repo commit-level semver major still works'
+git tag -a '2.0.0' -m TAG >/dev/null 2>&1
+
+add_repo_squash_history 1 'semver:minor'
+test_new './' '2.1.0' 'squash-merged PR classified via semver minor label'
+git tag -a '2.1.0' -m TAG >/dev/null 2>&1
+
+add_repo_squash_history 1
+test_new './' '599' 'unlabeled squash-merged PR is not classified'
+add_repo_history 1 fix
+test_new './' '2.1.1' 'unlabeled squash merge ignored alongside fix merge'
+git tag -a '2.1.1' -m TAG >/dev/null 2>&1
+
+test_previous 'A' 'A_0.0.0' 'directory initializes independently'
+test_new 'A' '599' 'directory without qualifying merges'
+
+test_previous 'B' 'B_0.0.0' 'directory initializes independently'
+add_history 'B' 3 ops
+test_new 'B' 'B_0.0.3' 'patches +3'
+git tag -a 'B_0.0.3' -m TAG >/dev/null 2>&1
+test_previous 'B' 'B_0.0.3' 'previous version tagged'
+
+test_previous 'A' 'A_0.0.0' 'newer B tag does not affect A history'
+
+test_previous 'C' 'C_0.0.0' 'directory initializes independently'
+add_history 'C' 5 feature
+test_new 'C' 'C_0.5.0' 'features +5'
+git tag -a 'C_0.5.0' -m TAG >/dev/null 2>&1
+
+add_history 'C' 1 feature 'semver:patch'
+test_new 'C' 'C_0.5.1' 'semver patch label overrides feature branch'
+git tag -a 'C_0.5.1' -m TAG >/dev/null 2>&1
+
+add_history 'C' 1 ops 'semver:minor'
+test_new 'C' 'C_0.6.0' 'semver minor label overrides ops branch'
+git tag -a 'C_0.6.0' -m TAG >/dev/null 2>&1
+
+add_history 'C' 1 ops 'semver:major'
+test_new 'C' 'C_1.0.0' 'semver major label overrides ops branch'
+git tag -a 'C_1.0.0' -m TAG >/dev/null 2>&1
+
+add_major_commit 'C'
+test_new 'C' 'C_2.0.0' 'commit-level semver major still works'
+
+test_previous 'D' 'D_0.0.0' 'directory initializes independently'
+add_history 'D' 2 feature
+add_history 'D' 3 enhancement
+test_new 'D' 'D_0.5.0' 'feature and enhancement branches increment minor versions'
+git tag -a 'D_0.5.0' -m TAG >/dev/null 2>&1
+
+test_previous 'E' 'E_0.0.0' 'directory initializes independently'
+add_history 'E' 1 fix
+add_history 'E' 1 bugfix
+add_history 'E' 1 hotfix
+add_history 'E' 1 ops
+test_new 'E' 'E_0.0.4' 'fix, bugfix, hotfix, and ops branches increment patch versions'
+git tag -a 'E_0.0.4' -m TAG >/dev/null 2>&1
+
+test_previous 'F' 'F_0.0.0' 'directory initializes independently'
+add_history 'F' 1 enhancement 'semver:patch'
+test_new 'F' 'F_0.0.1' 'semver patch label overrides enhancement minor branch'
+git tag -a 'F_0.0.1' -m TAG >/dev/null 2>&1
+
+add_history 'F' 1 fix 'semver:minor'
+test_new 'F' 'F_0.1.0' 'semver minor label overrides fix patch branch'
+git tag -a 'F_0.1.0' -m TAG >/dev/null 2>&1
+
+add_history 'F' 1 hotfix 'semver:major'
+test_new 'F' 'F_1.0.0' 'semver major label overrides hotfix patch branch'
+
+test_new_full './' '3.0.0' 'parallel full re-evaluation detects repo major'
+test_new_full 'F' 'F_1.0.0' 'parallel full re-evaluation detects directory major'
+
+if [[ -n "$FAILURE" ]]; then
+ exit 1
+fi