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