Repository: nico2sh/semtag Branch: master Commit: 1600023e1c3a Files: 7 Total size: 80.4 KB Directory structure: gitextract_v776qutd/ ├── .gitignore ├── LICENSE ├── README.md ├── release.sh ├── semtag ├── semtag_bkp └── test_semtag.sh ================================================ FILE CONTENTS ================================================ ================================================ FILE: .gitignore ================================================ ### Vim ### # swap [._]*.s[a-w][a-z] [._]s[a-w][a-z] # session Session.vim # temporary .netrwhist *~ # auto-generated tag files tags # if I open this with Intellij .idea ================================================ FILE: LICENSE ================================================ Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS APPENDIX: How to apply the Apache License to your work. To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. Copyright [yyyy] [name of copyright owner] Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. ================================================ FILE: README.md ================================================ # Semtag Semantic Tagging Script for Git [Version: v0.2.0] Notes: *This script is inspired by the [Nebula Release Plugin](https://github.com/nebula-plugins/nebula-release-plugin), and borrows a couple of lines from [Semver Bash Tool](https://github.com/fsaintjacques/semver-tool) (mostly the version comparison and the semantic version regex).* [A quick history of this script](https://medium.com/@dr_notsokind/semantic-tagging-with-git-1254dbded22) This is a script to help out version bumping on a project following the [Semantic Versioning](http://semver.org/) specification. It uses Git Tags to keep track the versions and the commit log between them, so no extra files are needed. It can be combined with release scripts, git hooks, etc, to have a consistent versioning. ### Why Bash? (and requirements) Portability, mostly. You can use the script in any project that uses Git as a version control system. The only requirement is Git. ### Why not use the Nebula-release plugin? Nebula Release is for releasing and publishing components and tries to automate the whole process from tagging to publishing. The goal of the `semtag` script is to only tag release versions, leaving the release process up to the developer. Plus, the `semtag` script doesn't depend on the build system (so no need to use Gradle), so it can be used in any project. ## Usage Copy the `semtag` script in your project's directory. Semtag distinguishes between final versions and non-final versions. Possible non-final versions are `alpha`, `beta` and `rc` (release candidate). Starts from version `0.0.0`, so the first time you initialize a version, it will tag it with the following bumped one (`1.0.0` if major, `0.1.0` if minor, `0.0.1` if patch) Use the script as follows: ``` semtag ``` Info commands: * `getfinal` Returns the current final version. * `getlast` Returns the last tagged version, it can be the final version or a non-final version. * `getcurrent` Returns the current version, it can be the tagged final version or a tagged non-final version. If there are unstaged or uncommitted changes, they will be included in the version, following this format: `..-dev.#+.`. Where `#` is the number of commits since the last final release, `branch` will be the current branch if we are not in the default branch (`master`, `main`, or other) and `hash` is the git hash of the current commit. * `get` Returns both last tagged version and current final version. Versioning commands: * `final` Bumps the version top a final version * `alpha` Bumps the version top an alpha version (appending `-alpha.#` to the version. * `beta` Bumps the version top a beta version (appending `-beta.#` to the version. * `candidate` Bumps the version top an release candidate version (appending `-rc.#` to the version. Note: If there are no commits since the last final version, the version is not bumped. All versioning commands tags the project with the new version using annotated tags (the tag message contains the list of commits included in the tag), and pushes the tag to the origin remote. If you don't want to tag, but just display which would be the next bumped version, use the flag `-o` for showing the output only. For specifying the scope you want to bump the version, use the `-s ` option. Possible scopes are `major`, `minor` and `patch`. There is also `auto` which will choose between `minor` and `patch` depending on the percentage of lines changed. Usually it should be the developers decisions which scope to use, since the percentage of lines is not a great criteria, but this option is to help giving a more meaningful versioning when using in automatic scripts. If you want to manually set a version, use the `-v ` option. Version must comply the semantic versioning specification (`v..`), and must be higher than the latest version. Works with any versioning command. ### Usage Examples See the `release` script as an example. The script gets the next version to tag, uses that version to update the `README.md` file (this one!), and the script's. Then commits the changes, and finally tags the project with this latest version. #### Gradle example For setting up your project's version, in your `build.gradle` file, add the following: ``` version=getVersionTag() def getVersionTag() { def hashStdOut = new ByteArrayOutputStream() exec { commandLine "$rootProject.projectDir/semtag", "getcurrent" standardOutput = hashStdOut } return hashStdOut.toString().trim() } ``` This way, the project's version every time you make a build, will be aligned with the tagged version. On your CI script, you can tag the release version before deploying, or alternatively, before publishing to a central repository (such as Artifactory), you can create a Gradle task tagging the release version: ``` def tagFinalVersion() { exec { commandLine "$rootProject.projectDir/semtag", "final", "-s minor" standardOutput = hashStdOut } doLast { project.version=getVersionTag() } } artifactoryPublish.dependsOn tagFinalVersion ``` Or create your own task for tagging and releasing. The goal of this script is to provide flexibility on how to manage and deal with the releases and deploys. ## How does it bump Semtag tries to guess which is the following version by using the current final version as a reference for bumping. For example: ``` $ semtag get Current final version: v1.0.0 Last tagged version: v1.0.0 $ semtag candidate -s minor $ semtag get Current final version: v1.0.0 Last tagged version: v1.1.0-rc.1 ``` Above it used the `v1.0.0` version for bumping a minor release candidate. If we try to increase a patch: ``` $ semtag candidate -s patch $ semtag get Current final version: v1.0.0 Last tagged version: v1.1.0-rc.2 ``` Again, it used the `v1.0.0` version as a reference to increase the patch version (so it should be bumped to `v1.0.1-rc.1`), but since the last tagged version is higher, it bumped the release candidate number instead. If we release a beta version: ``` $ semtag beta -s patch $ semtag get Current final version: v1.0.0 Last tagged version: v1.1.1-beta.1 ``` Now the patch has been bumped, since a beta version is considered to be lower than a release candidate, so is the verison number that bumps up, using the provided scope (`patch` in this case). ### Forcing a tag Semtag doesn't tag if there are no new commits since the last version, or if there are unstaged changes. To force to tag, use the `-f` flag, then it will bump no matter if there are unstaged changes or no new commits. ### Version prefix By default, semtag prefixes new versions with `v`. Use the `-p` (plain) flag to create new versions with no `v` prefix. ### Tag prefix For projects managing multiple components in a single repository, you can use custom tag prefixes to organize versions independently. Use the `-P ` flag to add a custom prefix to your tags. ```bash # Default behavior semtag alpha -s patch # Creates: v0.0.1-alpha.1 # With custom prefix semtag alpha -s patch -P service # Creates: service-v0.0.1-alpha.1 # Combine with -p flag for plain versions (no 'v') semtag alpha -s patch -P api -p # Creates: api-0.0.1-alpha.1 ``` Each tag prefix maintains its own independent version sequence, allowing you to version multiple components separately: ```bash # Version the backend service semtag final -s minor -P backend # Creates: backend-v0.1.0 # Version the frontend independently semtag final -s patch -P frontend # Creates: frontend-v0.0.1 # Default tags remain separate semtag final -s major # Creates: v1.0.0 ``` Query versions by prefix: ```bash semtag getlast -P backend # Returns: backend-v0.1.0 semtag getfinal -P frontend # Returns: frontend-v0.0.1 semtag getlast # Returns: v1.0.0 ``` License ======= Copyright 2020 Nico Hormazábal Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. ================================================ FILE: release.sh ================================================ #!/usr/bin/env bash # release.sh - Release automation script (Refactored) # Improved version with better error handling, validation, and maintainability set -euo pipefail # Fail fast on errors, undefined variables, and pipe failures readonly SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" readonly SEMTAG_SCRIPT="${SCRIPT_DIR}/semtag" readonly README_FILE="${SCRIPT_DIR}/README.md" # Configuration with defaults declare -A CONFIG=( [scope]="${1:-auto}" [dry_run]="false" [verbose]="false" ) # Utility functions log() { echo "[$(date +'%Y-%m-%d %H:%M:%S')] $*" >&2 } error() { log "ERROR: $*" exit 1 } verbose() { [[ "${CONFIG[verbose]}" == "true" ]] && log "$*" } # Validate dependencies check_dependencies() { if [[ ! -f "$SEMTAG_SCRIPT" ]]; then error "semtag script not found at: $SEMTAG_SCRIPT" fi if [[ ! -x "$SEMTAG_SCRIPT" ]]; then log "Making semtag executable..." chmod +x "$SEMTAG_SCRIPT" fi if [[ ! -f "$README_FILE" ]]; then error "README.md not found at: $README_FILE" fi # Check if we're in a git repository if ! git rev-parse --git-dir >/dev/null 2>&1; then error "Not in a git repository" fi # Check for uncommitted changes (unless dry run) if [[ "${CONFIG[dry_run]}" == "false" ]]; then if ! git diff --quiet HEAD 2>/dev/null; then error "Repository has uncommitted changes. Commit or stash them first." fi fi } # Validate scope parameter validate_scope() { local scope="${CONFIG[scope]}" case "$scope" in auto|major|minor|patch) verbose "Using scope: $scope" ;; *) error "Invalid scope '$scope'. Must be one of: auto, major, minor, patch" ;; esac } # Get next version without tagging get_next_version() { local version verbose "Getting next version with scope '${CONFIG[scope]}'..." # Use the semtag script to get next version (dry run mode) if ! version=$("$SEMTAG_SCRIPT" final -o -s "${CONFIG[scope]}" 2>/dev/null); then error "Failed to get next version from semtag" fi if [[ -z "$version" ]]; then error "semtag returned empty version" fi verbose "Next version will be: $version" echo "$version" } # Update version in files update_version_in_files() { local version="$1" log "Updating version to $version in source files..." # Create backup function for safer file operations backup_file() { local file="$1" if [[ -f "$file" ]]; then cp "$file" "${file}.backup" verbose "Created backup: ${file}.backup" fi } # Restore backup function restore_backup() { local file="$1" if [[ -f "${file}.backup" ]]; then mv "${file}.backup" "$file" log "Restored backup for $file" fi } # Clean backup function cleanup_backup() { local file="$1" if [[ -f "${file}.backup" ]]; then rm "${file}.backup" verbose "Cleaned up backup: ${file}.backup" fi } # Trap to cleanup backups on error trap 'restore_backup "$SEMTAG_SCRIPT"; restore_backup "$README_FILE"' ERR # Update semtag script version backup_file "$SEMTAG_SCRIPT" if ! sed -i '' "s/^PROG_VERSION=\"[^\"]*\"/PROG_VERSION=\"$version\"/g" "$SEMTAG_SCRIPT"; then error "Failed to update version in $SEMTAG_SCRIPT" fi verbose "Updated version in semtag script" # Update README.md version backup_file "$README_FILE" if ! sed -i '' "s/^\[Version: [^]]*\]/[Version: $version]/g" "$README_FILE"; then error "Failed to update version in $README_FILE" fi verbose "Updated version in README.md" # Verify changes were made if ! grep -q "PROG_VERSION=\"$version\"" "$SEMTAG_SCRIPT"; then error "Version update verification failed for $SEMTAG_SCRIPT" fi if ! grep -q "\\[Version: $version\\]" "$README_FILE"; then error "Version update verification failed for $README_FILE" fi # Clean up backups on success cleanup_backup "$SEMTAG_SCRIPT" cleanup_backup "$README_FILE" # Disable trap trap - ERR log "Successfully updated version in all files" } # Commit and push changes commit_and_push_changes() { local version="$1" log "Committing changes for version $version..." # Stage the modified files if ! git add "$SEMTAG_SCRIPT" "$README_FILE"; then error "Failed to stage modified files" fi # Commit with descriptive message local commit_message="Update version info to $version - Updated PROG_VERSION in semtag script - Updated version badge in README.md Automated release preparation commit." if ! git commit -m "$commit_message"; then error "Failed to commit changes" fi verbose "Changes committed successfully" # Push changes log "Pushing changes to remote..." if ! git push; then error "Failed to push changes to remote" fi log "Changes pushed successfully" } # Create final tag create_final_tag() { local version="$1" log "Creating final tag for version $version..." # Use semtag to create the final tag if ! "$SEMTAG_SCRIPT" final -f -v "$version"; then error "Failed to create final tag with semtag" fi log "Successfully created and pushed tag: $version" } # Show release summary show_summary() { local version="$1" log "Release Summary:" log " Version: $version" log " Scope: ${CONFIG[scope]}" log " Files updated: semtag, README.md" log " Git tag created and pushed: $version" log "" log "Release completed successfully!" } # Dry run mode perform_dry_run() { local version version=$(get_next_version) log "DRY RUN MODE - No changes will be made" log "Would release version: $version" log "Would update files: $SEMTAG_SCRIPT, $README_FILE" log "Would commit and push changes" log "Would create and push git tag: $version" } # Parse command line arguments parse_arguments() { while [[ $# -gt 0 ]]; do case $1 in --dry-run) CONFIG[dry_run]="true" shift ;; --verbose|-v) CONFIG[verbose]="true" shift ;; --help|-h) show_help exit 0 ;; -*) error "Unknown option: $1" ;; *) # Assume it's a scope parameter CONFIG[scope]="$1" shift ;; esac done } # Show help show_help() { cat << EOF Usage: $(basename "$0") [SCOPE] [OPTIONS] Release automation script for semtag. Arguments: SCOPE Version scope to bump (auto|major|minor|patch) [default: auto] Options: --dry-run Show what would be done without making changes --verbose, -v Enable verbose output --help, -h Show this help message Examples: $(basename "$0") # Auto-detect scope and release $(basename "$0") minor # Release with minor version bump $(basename "$0") --dry-run # Preview release actions $(basename "$0") patch --verbose # Release patch with verbose output This script will: 1. Get the next version using semtag 2. Update version strings in semtag and README.md 3. Commit and push the changes 4. Create and push a git tag using semtag EOF } # Main execution function main() { # Parse arguments (excluding the scope which is handled in CONFIG initialization) shift 2>/dev/null || true # Remove scope from $@ if present parse_arguments "$@" log "Starting release process with scope: ${CONFIG[scope]}" # Validate everything first check_dependencies validate_scope # Handle dry run if [[ "${CONFIG[dry_run]}" == "true" ]]; then perform_dry_run return 0 fi # Execute release process local version version=$(get_next_version) update_version_in_files "$version" commit_and_push_changes "$version" create_final_tag "$version" show_summary "$version" } # Only run main if script is executed directly (not sourced) if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then main "$@" fi ================================================ FILE: semtag ================================================ #!/usr/bin/env bash # Enable strict error handling set -euo pipefail PROG=semtag PROG_VERSION="v0.1.2" # Error handling function error_exit() { local error_message="$1" local exit_code="${2:-1}" echo "ERROR: $error_message" >&2 exit "$exit_code" } # Check if we're in a git repository check_git_repo() { if ! git rev-parse --git-dir >/dev/null 2>&1; then error_exit "Not in a git repository. Please run this script from within a git repository." fi } SEMVER_REGEX="^v?(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)(\-[0-9A-Za-z-]+(\.[0-9A-Za-z-]+)*)?(\+[0-9A-Za-z-]+(\.[0-9A-Za-z-]+)*)?$" IDENTIFIER_REGEX="^\-[0-9A-Za-z-]+(\.[0-9A-Za-z-]+)*$" NUMERIC_REGEX='^[0-9]+$' # Length limit for the branch name in build metadata. MAX_BRANCH_LENGTH=50 # Global variables FIRST_VERSION="v0.0.0" finalversion=$FIRST_VERSION lastversion=$FIRST_VERSION hasversiontag="false" scope="patch" displayonly="false" forcetag="false" prefix="v" tag_prefix="" forcedversion= versionname= identifier= HELP="\ Usage: $PROG $PROG getlast $PROG getfinal $PROG (final|alpha|beta|candidate) [-s (major|minor|patch|auto) | -o] $PROG --help $PROG --version Options: -s The scope that must be increased, can be major, minor or patch. The resulting version will match X.Y.Z(-PRERELEASE)(+BUILD) where X, Y and Z are positive integers, PRERELEASE is an optionnal string composed of alphanumeric characters describing if the build is a release candidate, alpha or beta version, with a number. BUILD is also an optional string composed of alphanumeric characters and hyphens. Setting the scope as 'auto', the script will chose the scope between 'minor' and 'patch', depending on the amount of lines added (<10% will choose patch). -v Specifies manually the version to be tagged, must be a valid semantic version in the format X.Y.Z where X, Y and Z are positive integers. -o Output the version only, shows the bumped version, but doesn't tag. -f Forces to tag, even if there are unstaged or uncommited changes. -p Use a plain version, ie. do not prefix with 'v'. -P Add a custom prefix to the tag (e.g., -P service generates service-v0.0.1). Commands: --help Print this help message. --version Prints the program's version. get Returns both current final version and last tagged version. getlast Returns the latest tagged version. getfinal Returns the latest tagged final version. getcurrent Returns the current version, based on the latest one, if there are uncommited or unstaged changes, they will be reflected in the version, adding the number of pending commits, current branch and commit hash. final Tags the current build as a final version, this only can be done on the default branch. candidate Tags the current build as a release candidate, the tag will contain all the commits from the last final version. alpha Tags the current build as an alpha version, the tag will contain all the commits from the last final version. beta Tags the current build as a beta version, the tag will contain all the commits from the last final version." # Validate input arguments validate_scope() { local scope="$1" case "$scope" in major|minor|patch|auto) ;; *) error_exit "Invalid scope '$scope'. Valid scopes are: major, minor, patch, auto" ;; esac } validate_version() { local version="$1" if [[ ! "$version" =~ $SEMVER_REGEX ]]; then error_exit "Invalid version format '$version'. Must follow semantic versioning (e.g., 1.2.3)" fi } # Commands and options ACTION="${1:-getlast}" if [[ $# -gt 0 ]]; then shift fi # We get the parameters while getopts "v:s:ofpP:" opt; do case $opt in v) forcedversion="$OPTARG" validate_version "$forcedversion" ;; s) scope="$OPTARG" validate_scope "$scope" ;; o) displayonly="true" ;; f) forcetag="true" ;; p) prefix="" ;; P) tag_prefix="$OPTARG-" ;; \?) error_exit "Invalid option: -$OPTARG" ;; :) error_exit "Option -$OPTARG requires an argument." ;; esac done # Try to programmatically fetch the default branch. Go by the first remote HEAD found, otherwise default to `master`. # $1 The variable to store the result function get_default_branch { local __result="$1" local __default_branch="" local __remotes if ! __remotes=$(git remote 2>/dev/null); then error_exit "Failed to get git remotes" fi if [[ -n "$__remotes" ]]; then while IFS= read -r __remote; do local __default_branch_ref if __default_branch_ref=$(git symbolic-ref --quiet "refs/remotes/${__remote}/HEAD" 2>/dev/null); then __default_branch="${__default_branch_ref#refs/remotes/${__remote}/}" if [[ -n "$__default_branch" ]]; then break fi fi done <<< "$__remotes" fi printf -v "$__result" '%s' "${__default_branch:-master}" } # Gets a string with the version and returns an array of maximum size of 5 with all the parts of the sematinc version # $1 The string containing the version in semantic format # $2 The variable to store the result array: # position 0: major number # position 1: minor number # position 2: patch number # position 3: identifier (or prerelease identifier) # position 4: build info function explode_version { local __version="$1" local __result="$2" if [[ "$__version" =~ $SEMVER_REGEX ]] ; then local __major="${BASH_REMATCH[1]}" local __minor="${BASH_REMATCH[2]}" local __patch="${BASH_REMATCH[3]}" local __prere="${BASH_REMATCH[4]}" local __build="${BASH_REMATCH[5]}" # Use eval for compatibility with older bash versions eval "$__result=(\"\$__major\" \"\$__minor\" \"\$__patch\" \"\$__prere\" \"\$__build\")" else # Clear the array eval "$__result=()" fi } # Compare two versions and returns -1, 0 or 1 # $1 The first version to compare # $2 The second version to compare # $3 The variable where to store the result function compare_versions { local __first __second explode_version "$1" __first explode_version "$2" __second local lv="$3" # Compares MAJOR, MINOR and PATCH for i in 0 1 2; do local __numberfirst="${__first[$i]:-0}" local __numbersecond="${__second[$i]:-0}" local __diff=$(((__numberfirst) - (__numbersecond))) case "$__diff" in 0) ;; -*) printf -v "$lv" '%s' "-1" return 0 ;; *) printf -v "$lv" '%s' "1" return 0 ;; esac done # Identifiers should compare with the ASCII order. local compareresult compare_identifiers "${__first[3]:-}" "${__second[3]:-}" compareresult printf -v "$lv" '%s' "$compareresult" } # Returns the number comparison # $1 The first number to compare # $2 The second number to compare # $3 The variable where to store the result function compare_numeric { local __first="$1" local __second="$2" local __result="$3" if (( __first < __second )) ; then printf -v "$__result" '%s' "-1" elif (( __first > __second )) ; then printf -v "$__result" '%s' "1" else printf -v "$__result" '%s' "0" fi } # Returns the alpanumeric comparison # $1 The first alpanumeric to compare # $2 The second alpanumeric to compare # $3 The variable where to store the result function compare_alphanumeric { local __first="$1" local __second="$2" local __result="$3" if [[ "$__first" < "$__second" ]] ; then printf -v "$__result" '%s' "-1" elif [[ "$__first" > "$__second" ]] ; then printf -v "$__result" '%s' "1" else printf -v "$__result" '%s' "0" fi } # Returns the last version of two # $1 The first version to compare # $2 The second version to compare # $3 The variable where to store the last one function get_latest_of_two { local __first="$1" local __second="$2" local __result local __latest="$3" compare_versions "$__first" "$__second" __result case "$__result" in 0) printf -v "$__latest" '%s' "$__second" ;; -1) printf -v "$__latest" '%s' "$__second" ;; 1) printf -v "$__latest" '%s' "$__first" ;; esac } # Returns comparison of two identifier parts # $1 The first part to compare # $2 The second part to compare # $3 The variable where to store the compare result function compare_identifier_part { local __first=$1 local __second=$2 local __result=$3 local compareresult if [[ "$__first" =~ $NUMERIC_REGEX ]] && [[ "$__second" =~ $NUMERIC_REGEX ]] ; then compare_numeric "$__first" "$__second" compareresult eval "$__result=$compareresult" return 0 fi compare_alphanumeric "$__first" "$__second" compareresult eval "$__result=$compareresult" } # Returns comparison of two identifiers # $1 The first identifier to compare # $2 The second identifier to compare # $3 The variable where to store the compare result function compare_identifiers { local __first=$1 local __second=$2 local __result=$3 local partresult local arraylengths if [[ -n "$__first" ]] && [[ -n "$__second" ]]; then explode_identifier "${__first}" explodedidentifierfirst explode_identifier "${__second}" explodedidentifiersecond firstsize=${#explodedidentifierfirst[@]} secondsize=${#explodedidentifiersecond[@]} minlength=$(( $firstsize<$secondsize ? $firstsize : $secondsize )) for (( i = 0 ; i < $minlength ; i++ )); do compare_identifier_part "${explodedidentifierfirst[$i]}" "${explodedidentifiersecond[$i]}" partresult case $partresult in 0) ;; *) eval "$__result=$partresult" return 0 ;; esac done compare_numeric $firstsize $secondsize arraylengths eval "$__result=$arraylengths" return 0 elif [[ -z "$__first" ]] && [[ -n "$__second" ]]; then eval "$__result=1" return 0 elif [[ -n "$__first" ]] && [[ -z "$__second" ]]; then eval "$__result=-1" return 0 fi eval "$__result=0" } # Assigns a 2 size array with the identifier, having the identifier at pos 0, and the number in pos 1 # $1 The identifier in the format -id.# # $2 The vferiable where to store the 2 size array function explode_identifier { local __identifier="$1" local __result="$2" if [[ "$__identifier" =~ $IDENTIFIER_REGEX ]] ; then local identifierparts IFS='-.' read -ra identifierparts <<< "$__identifier" # Filter out empty elements (the first element is empty when splitting "-beta.1") local filtered=() for part in "${identifierparts[@]}"; do if [[ -n "$part" ]]; then filtered+=("$part") fi done eval "$__result=( \"\${filtered[@]}\" )" else eval "$__result=()" fi } # Gets a list of tags and assigns the base and latest versions # Receives an array with the tags containing the versions # Assigns to the global variables finalversion and lastversion the final version and the latest version function get_latest { local __taglist=("$@") local __tagsnumber=${#__taglist[@]} local __current case $__tagsnumber in 0) finalversion="$FIRST_VERSION" lastversion="$FIRST_VERSION" ;; 1) __current="${__taglist[0]}" local ver explode_version "$__current" ver if [ ${#ver[@]} -gt 0 ]; then if [ -n "${ver[3]:-}" ]; then finalversion="$FIRST_VERSION" else finalversion="$__current" fi lastversion="$__current" else finalversion="$FIRST_VERSION" lastversion="$FIRST_VERSION" fi ;; *) local __lastpos=$((__tagsnumber-1)) for i in $(seq 0 "$__lastpos") do __current="${__taglist[i]}" local ver explode_version "$__current" ver if [ ${#ver[@]} -gt 0 ]; then if [ -z "${ver[3]:-}" ]; then get_latest_of_two "$finalversion" "$__current" finalversion get_latest_of_two "$lastversion" "$finalversion" lastversion else get_latest_of_two "$lastversion" "$__current" lastversion fi fi done ;; esac if git rev-parse -q --verify "refs/tags/$tag_prefix$lastversion" >/dev/null 2>&1; then hasversiontag="true" else hasversiontag="false" fi } # Gets the next version given the provided scope # $1 The version that is going to be bumped # $2 The scope to bump # $3 The variable where to stoer the result function get_next_version { local __exploded local __fromversion="$1" local __scope="$2" local __result="$3" explode_version "$__fromversion" __exploded case "$__scope" in major) __exploded[0]=$((${__exploded[0]}+1)) __exploded[1]=0 __exploded[2]=0 ;; minor) __exploded[1]=$((${__exploded[1]}+1)) __exploded[2]=0 ;; patch) __exploded[2]=$((${__exploded[2]}+1)) ;; esac printf -v "$__result" '%s' "${prefix}${__exploded[0]}.${__exploded[1]}.${__exploded[2]}" } function bump_version { ## First we try to get the next version based on the existing last one if [ "$scope" == "auto" ]; then get_scope_auto scope fi local __candidatefromlast=$FIRST_VERSION local __explodedlast explode_version $lastversion __explodedlast if [[ -n "${__explodedlast[3]}" ]]; then # Last version is not final local __idlast explode_identifier ${__explodedlast[3]} __idlast # We get the last, given the desired id based on the scope __candidatefromlast="${prefix}${__explodedlast[0]}.${__explodedlast[1]}.${__explodedlast[2]}" if [[ -n "$identifier" ]]; then local __nextid="$identifier.1" if [ "$identifier" == "${__idlast[0]}" ]; then # We target the same identifier as the last so we increase one __nextid="$identifier.$(( ${__idlast[1]}+1 ))" __candidatefromlast="$__candidatefromlast-$__nextid" else # Different identifiers, we make sure we are assigning a higher identifier, if not, we increase the version __candidatefromlast="$__candidatefromlast-$__nextid" local __comparedwithlast compare_versions $__candidatefromlast $lastversion __comparedwithlast if [ "$__comparedwithlast" == -1 ]; then get_next_version $__candidatefromlast $scope __candidatefromlast __candidatefromlast="$__candidatefromlast-$__nextid" fi fi fi fi # Then we try to get the version based on the latest final one local __candidatefromfinal get_next_version $finalversion $scope __candidatefromfinal if [[ -n "$identifier" ]]; then __candidatefromfinal="$__candidatefromfinal-$identifier.1" fi # Finally we compare both candidates local __resultversion local __result compare_versions $__candidatefromlast $__candidatefromfinal __result case $__result in 0) __resultversion=$__candidatefromlast ;; -1) __resultversion="$__candidatefromfinal" ;; 1) __resultversion=$__candidatefromlast ;; esac printf -v "$1" '%s' "$__resultversion" } function increase_version { local __version="" if [ -z "${forcedversion:-}" ]; then bump_version __version else if [[ "$forcedversion" =~ $SEMVER_REGEX ]] ; then local __result compare_versions "$forcedversion" "$lastversion" __result if [ "$__result" -le 0 ]; then error_exit "Version '$forcedversion' can't be lower than or equal to last version: $lastversion" fi else error_exit "Invalid version format: $forcedversion" fi __version="$forcedversion" fi # Add the tag_prefix to the final version for output or tagging __version="${tag_prefix}${__version}" if [ "$displayonly" == "true" ]; then echo "$__version" else if [ "$forcetag" == "false" ]; then check_git_dirty_status fi local __commitlist if [ "$finalversion" == "$FIRST_VERSION" ] || [ "$hasversiontag" != "true" ]; then if ! __commitlist=$(git log --pretty=oneline 2>/dev/null); then error_exit "Failed to get git commit log" fi else if ! __commitlist=$(git log --pretty=oneline "$tag_prefix$finalversion"... 2>/dev/null); then error_exit "Failed to get git commit log from $tag_prefix$finalversion" fi fi # If we are forcing a bump, we add bump to the commit list if [[ -z "$__commitlist" && "$forcetag" == "true" ]]; then __commitlist="bump" fi if [[ -z "$__commitlist" ]]; then echo "No commits since the last final version, not bumping version" else if [[ -z "${versionname:-}" ]]; then if ! versionname=$(date -u +"%Y-%m-%dT%H:%M:%SZ" 2>/dev/null); then error_exit "Failed to generate timestamp for version name" fi fi local __message="$versionname $__commitlist" # We check we have info on the user local __username if ! __username=$(git config user.name 2>/dev/null); then if ! __username=$(id -u -n 2>/dev/null); then error_exit "Failed to get username" fi if ! git config user.name "$__username" 2>/dev/null; then error_exit "Failed to set git user.name" fi fi local __useremail if ! __useremail=$(git config user.email 2>/dev/null); then if ! __useremail=$(hostname 2>/dev/null); then error_exit "Failed to get hostname for email" fi if ! git config user.email "$__username@$__useremail" 2>/dev/null; then error_exit "Failed to set git user.email" fi fi if ! git tag -a "$__version" -m "$__message" 2>/dev/null; then error_exit "Failed to create git tag: $__version" fi # If we have a remote, we push there local __remotes if __remotes=$(git remote 2>/dev/null) && [[ -n "$__remotes" ]]; then while IFS= read -r __remote; do if git push "$__remote" "$__version" >/dev/null 2>&1; then echo "$__version pushed to $__remote" else error_exit "Failed to push tag $__version to remote $__remote" fi done <<< "$__remotes" else echo "$__version" fi fi fi } function check_git_dirty_status { local __repostatus="" get_work_tree_status __repostatus if [ "$__repostatus" == "uncommitted" ]; then echo "ERROR: You have uncommitted changes" >&2 if git status --porcelain 2>/dev/null; then : # status displayed successfully else echo "Unable to display git status" >&2 fi exit 1 fi if [ "$__repostatus" == "unstaged" ]; then echo "ERROR: You have unstaged changes" >&2 if git status --porcelain 2>/dev/null; then : # status displayed successfully else echo "Unable to display git status" >&2 fi exit 1 fi } # Get the total amount of lines of code in the repo function get_total_lines { local __empty_id if ! __empty_id=$(git hash-object -t tree /dev/null 2>/dev/null); then error_exit "Failed to get empty tree hash" fi local __changes if ! __changes=$(git diff --numstat "$__empty_id" 2>/dev/null); then error_exit "Failed to get git diff numstat" fi local __added_deleted="$1" get_changed_lines "$__changes" "$__added_deleted" } # Get the total amount of lines of code since the provided tag function get_sincetag_lines { local __sincetag="$1" local __changes if ! __changes=$(git diff --numstat "$__sincetag" 2>/dev/null); then error_exit "Failed to get git diff numstat since $__sincetag" fi local __added_deleted="$2" get_changed_lines "$__changes" "$__added_deleted" } function get_changed_lines { local __changes_numstat="$1" local __result="$2" local __changes_array if [[ -n "$__changes_numstat" ]]; then IFS=$'\n' read -rd '' -a __changes_array <<<"$__changes_numstat" || true else __changes_array=() fi local __diff_regex="^([0-9]+)[[:space:]]+([0-9]+)[[:space:]]+.+$" local __total_added=0 local __total_deleted=0 for i in "${__changes_array[@]}" do if [[ "$i" =~ $__diff_regex ]] ; then local __added="${BASH_REMATCH[1]}" local __deleted="${BASH_REMATCH[2]}" __total_added=$(( __total_added + __added )) __total_deleted=$(( __total_deleted + __deleted )) fi done eval "$__result=( $__total_added $__total_deleted )" } function get_scope_auto { local __result="$1" local __verbose="${2:-}" local __total=() local __since=() local __scope="" get_total_lines __total get_sincetag_lines "$tag_prefix$finalversion" __since local __percentage=0 if [ "${__total[0]}" != "0" ]; then __percentage=$(( 100 * ${__since[0]} / ${__total[0]} )) if [ "$__percentage" -gt "10" ]; then __scope="minor" else __scope="patch" fi else __scope="patch" fi printf -v "$__result" '%s' "$__scope" if [[ -n "$__verbose" ]]; then echo "[Auto Scope] Percentage of lines changed: $__percentage" echo "[Auto Scope] : $__scope" fi } function get_work_tree_status { local __result="$1" # Update the index if ! git update-index -q --ignore-submodules --refresh >/dev/null 2>&1; then error_exit "Failed to update git index" fi printf -v "$__result" '%s' "" if ! git diff-files --quiet --ignore-submodules -- >/dev/null 2>&1; then printf -v "$__result" '%s' "unstaged" fi if ! git diff-index --cached --quiet HEAD --ignore-submodules -- >/dev/null 2>&1; then printf -v "$__result" '%s' "uncommitted" fi } function get_current { local __result="$1" local __commitcount if [ "$hasversiontag" == "true" ]; then if ! __commitcount=$(git rev-list "$tag_prefix$lastversion".. --count 2>/dev/null); then error_exit "Failed to get commit count from $tag_prefix$lastversion" fi else if ! __commitcount=$(git rev-list --count HEAD 2>/dev/null); then error_exit "Failed to get total commit count" fi fi local __status="" get_work_tree_status __status if [ "$__commitcount" == "0" ] && [ -z "$__status" ]; then printf -v "$__result" '%s' "$lastversion" else local __buildinfo if ! __buildinfo=$(git rev-parse --short HEAD 2>/dev/null); then error_exit "Failed to get short commit hash" fi local __currentbranch if ! __currentbranch=$(git rev-parse --abbrev-ref HEAD 2>/dev/null); then error_exit "Failed to get current branch name" fi # Safely truncate branch name __currentbranch="${__currentbranch:0:$MAX_BRANCH_LENGTH}" local default_branch get_default_branch default_branch if [ "$__currentbranch" != "$default_branch" ]; then __buildinfo="$__currentbranch.$__buildinfo" fi local __suffix="" if [ "$__commitcount" != "0" ]; then if [ -n "$__suffix" ]; then __suffix="$__suffix." fi __suffix="$__suffix$__commitcount" fi if [ -n "$__status" ]; then if [ -n "$__suffix" ]; then __suffix="$__suffix." fi __suffix="$__suffix$__status" fi __suffix="$__suffix+$__buildinfo" if [ "$lastversion" == "$finalversion" ]; then scope="patch" identifier="" local __bumped="" bump_version __bumped printf -v "$__result" '%s' "$__bumped-dev.$__suffix" else printf -v "$__result" '%s' "$lastversion.$__suffix" fi fi } function init { check_git_repo local TAGS if ! TAGS=$(git tag --merged 2>/dev/null); then error_exit "Failed to get git tags" fi local TAG_ARRAY if [[ -n "$TAGS" ]]; then IFS=$'\n' read -rd '' -a TAG_ARRAY <<<"$TAGS" || true else TAG_ARRAY=() fi # Filter and strip tag_prefix from tags local FILTERED_TAGS=() if [[ ${#TAG_ARRAY[@]} -gt 0 ]]; then for tag in "${TAG_ARRAY[@]}"; do if [[ "$tag" == "$tag_prefix"* ]]; then # Strip the tag_prefix from the tag local stripped_tag="${tag#$tag_prefix}" FILTERED_TAGS+=("$stripped_tag") fi done fi if [[ ${#FILTERED_TAGS[@]} -gt 0 ]]; then get_latest "${FILTERED_TAGS[@]}" else get_latest fi if ! currentbranch=$(git rev-parse --abbrev-ref HEAD 2>/dev/null); then error_exit "Failed to get current git branch" fi } case "$ACTION" in --help) echo -e "$HELP" ;; --version) echo -e "${PROG}: $PROG_VERSION" ;; final) init default_branch="" get_default_branch default_branch diff="" if ! diff=$(git diff "$default_branch" 2>/dev/null); then error_exit "Failed to compare with default branch: $default_branch" fi if [ "$forcetag" == "false" ]; then if [ -n "$diff" ]; then error_exit "Branch must be updated with $default_branch for final versions" fi fi increase_version ;; alpha|beta) init identifier="$ACTION" increase_version ;; candidate) init identifier="rc" increase_version ;; getlast) init echo "$tag_prefix$lastversion" ;; getfinal) init echo "$tag_prefix$finalversion" ;; getcurrent) init current="" get_current current echo "$current" ;; get) init echo "Current final version: $tag_prefix$finalversion" echo "Last tagged version: $tag_prefix$lastversion" ;; *) error_exit "'$ACTION' is not a valid command, see --help for available commands." ;; esac ================================================ FILE: semtag_bkp ================================================ #!/usr/bin/env bash # This is a deprecated version PROG=semtag PROG_VERSION="v0.1.2" SEMVER_REGEX="^v?(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)(\-[0-9A-Za-z-]+(\.[0-9A-Za-z-]+)*)?(\+[0-9A-Za-z-]+(\.[0-9A-Za-z-]+)*)?$" IDENTIFIER_REGEX="^\-[0-9A-Za-z-]+(\.[0-9A-Za-z-]+)*$" NUMERIC_REGEX='^[0-9]+$' # Length limit for the branch name in build metadata. MAX_BRANCH_LENGTH=50 # Global variables FIRST_VERSION="v0.0.0" finalversion=$FIRST_VERSION lastversion=$FIRST_VERSION hasversiontag="false" scope="patch" displayonly="false" forcetag="false" prefix="v" forcedversion= versionname= identifier= HELP="\ Usage: $PROG $PROG getlast $PROG getfinal $PROG (final|alpha|beta|candidate) [-s (major|minor|patch|auto) | -o] $PROG --help $PROG --version Options: -s The scope that must be increased, can be major, minor or patch. The resulting version will match X.Y.Z(-PRERELEASE)(+BUILD) where X, Y and Z are positive integers, PRERELEASE is an optionnal string composed of alphanumeric characters describing if the build is a release candidate, alpha or beta version, with a number. BUILD is also an optional string composed of alphanumeric characters and hyphens. Setting the scope as 'auto', the script will chose the scope between 'minor' and 'patch', depending on the amount of lines added (<10% will choose patch). -v Specifies manually the version to be tagged, must be a valid semantic version in the format X.Y.Z where X, Y and Z are positive integers. -o Output the version only, shows the bumped version, but doesn't tag. -f Forces to tag, even if there are unstaged or uncommited changes. -p Use a plain version, ie. do not prefix with 'v'. Commands: --help Print this help message. --version Prints the program's version. get Returns both current final version and last tagged version. getlast Returns the latest tagged version. getfinal Returns the latest tagged final version. getcurrent Returns the current version, based on the latest one, if there are uncommited or unstaged changes, they will be reflected in the version, adding the number of pending commits, current branch and commit hash. final Tags the current build as a final version, this only can be done on the default branch. candidate Tags the current build as a release candidate, the tag will contain all the commits from the last final version. alpha Tags the current build as an alpha version, the tag will contain all the commits from the last final version. beta Tags the current build as a beta version, the tag will contain all the commits from the last final version." # Commands and options ACTION="getlast" ACTION="$1" shift # We get the parameters while getopts "v:s:ofp" opt; do case $opt in v) forcedversion="$OPTARG" ;; s) scope="$OPTARG" ;; o) displayonly="true" ;; f) forcetag="true" ;; p) prefix="" ;; \?) echo "Invalid option: -$OPTARG" >&2 exit 1 ;; :) echo "Option -$OPTARG requires an argument." >&2 exit 1 ;; esac done # Try to programmatically fetch the default branch. Go by the first remote HEAD found, otherwise default to `master`. # $1 The variable to store the result function get_default_branch { local __result=$1 local __remotes=$(git remote) if [[ -n $__remotes ]]; then for __remote in $__remotes; do local __default_branch_ref=$(git symbolic-ref --quiet refs/remotes/${__remote}/HEAD || true) local __default_branch=${__default_branch_ref#refs/remotes/${__remote}/} if [[ -n ${__default_branch} ]]; then break fi done fi eval "${__result}=${__default_branch:-master}" } # Gets a string with the version and returns an array of maximum size of 5 with all the parts of the sematinc version # $1 The string containing the version in semantic format # $2 The variable to store the result array: # position 0: major number # position 1: minor number # position 2: patch number # position 3: identifier (or prerelease identifier) # position 4: build info function explode_version { local __version=$1 local __result=$2 if [[ $__version =~ $SEMVER_REGEX ]] ; then local __major=${BASH_REMATCH[1]} local __minor=${BASH_REMATCH[2]} local __patch=${BASH_REMATCH[3]} local __prere=${BASH_REMATCH[4]} local __build=${BASH_REMATCH[5]} eval "$__result=(\"$__major\" \"$__minor\" \"$__patch\" \"$__prere\" \"$__build\")" else eval "$__result=" fi } # Compare two versions and returns -1, 0 or 1 # $1 The first version to compare # $2 The second version to compare # $3 The variable where to store the result function compare_versions { local __first local __second explode_version $1 __first explode_version $2 __second local lv=$3 # Compares MAJOR, MINOR and PATCH for i in 0 1 2; do local __numberfirst=${__first[$i]} local __numbersecond=${__second[$i]} case $(($__numberfirst - $__numbersecond)) in 0) ;; -[0-9]*) eval "$lv=-1" return 0 ;; [0-9]*) eval "$lv=1" return 0 ;; esac done # Identifiers should compare with the ASCII order. local __identifierfirst=${__first[3]} local __identifiersecond=${__second[3]} compare_identifiers "${__first[3]}" "${__second[3]}" compareresult eval "$lv=$compareresult" } # Returns the number comparison # $1 The first number to compare # $2 The second number to compare # $3 The variable where to store the result function compare_numeric { local __first=$1 local __second=$2 local __result=$3 if (( "$__first" < "$__second" )) ; then eval "$__result=-1" elif (( "$__first" > "$__second" )) ; then eval "$__result=1" else eval "$__result=0" fi } # Returns the alpanumeric comparison # $1 The first alpanumeric to compare # $2 The second alpanumeric to compare # $3 The variable where to store the result function compare_alphanumeric { local __first=$1 local __second=$2 local __result=$3 if [[ "$__first" < "$__second" ]] ; then eval "$__result=-1" elif [[ "$__first" > "$__second" ]] ; then eval "$__result=1" else eval "$__result=0" fi } # Returns the last version of two # $1 The first version to compare # $2 The second version to compare # $3 The variable where to store the last one function get_latest_of_two { local __first=$1 local __second=$2 local __result local __latest=$3 compare_versions $__first $__second __result case $__result in 0) eval "$__latest=$__second" ;; -1) eval "$__latest=$__second" ;; 1) eval "$__latest=$__first" ;; esac } # Returns comparison of two identifier parts # $1 The first part to compare # $2 The second part to compare # $3 The variable where to store the compare result function compare_identifier_part { local __first=$1 local __second=$2 local __result=$3 local compareresult if [[ "$__first" =~ $NUMERIC_REGEX ]] && [[ "$__second" =~ $NUMERIC_REGEX ]] ; then compare_numeric "$__first" "$__second" compareresult eval "$__result=$compareresult" return 0 fi compare_alphanumeric "$__first" "$__second" compareresult eval "$__result=$compareresult" } # Returns comparison of two identifiers # $1 The first identifier to compare # $2 The second identifier to compare # $3 The variable where to store the compare result function compare_identifiers { local __first=$1 local __second=$2 local __result=$3 local partresult local arraylengths if [[ -n "$__first" ]] && [[ -n "$__second" ]]; then explode_identifier "${__first}" explodedidentifierfirst explode_identifier "${__second}" explodedidentifiersecond firstsize=${#explodedidentifierfirst[@]} secondsize=${#explodedidentifiersecond[@]} minlength=$(( $firstsize<$secondsize ? $firstsize : $secondsize )) for (( i = 0 ; i < $minlength ; i++ )); do compare_identifier_part "${explodedidentifierfirst[$i]}" "${explodedidentifiersecond[$i]}" partresult case $partresult in 0) ;; *) eval "$__result=$partresult" return 0 ;; esac done compare_numeric $firstsize $secondsize arraylengths eval "$__result=$arraylengths" return 0 elif [[ -z "$__first" ]] && [[ -n "$__second" ]]; then eval "$__result=1" return 0 elif [[ -n "$__first" ]] && [[ -z "$__second" ]]; then eval "$__result=-1" return 0 fi eval "$__result=0" } # Assigns a 2 size array with the identifier, having the identifier at pos 0, and the number in pos 1 # $1 The identifier in the format -id.# # $2 The vferiable where to store the 2 size array function explode_identifier { local __identifier=$1 local __result=$2 if [[ $__identifier =~ $IDENTIFIER_REGEX ]] ; then IFS='-.' read -ra identifierparts <<< $__identifier eval "$__result=( ${identifierparts[@]} )" else eval "$__result=" fi } # Gets a list of tags and assigns the base and latest versions # Receives an array with the tags containing the versions # Assigns to the global variables finalversion and lastversion the final version and the latest version function get_latest { local __taglist=("$@") local __tagsnumber=${#__taglist[@]} local __current case $__tagsnumber in 0) finalversion=$FIRST_VERSION lastversion=$FIRST_VERSION ;; 1) __current=${__taglist[0]} explode_version $__current ver if [ -n "$ver" ]; then if [ -n "${ver[3]}" ]; then finalversion=$FIRST_VERSION else finalversion=$__current fi lastversion=$__current else finalversion=$FIRST_VERSION lastversion=$FIRST_VERSION fi ;; *) local __lastpos=$(($__tagsnumber-1)) for i in $(seq 0 $__lastpos) do __current=${__taglist[i]} explode_version ${__taglist[i]} ver if [ -n "$ver" ]; then if [ -z "${ver[3]}" ]; then get_latest_of_two $finalversion $__current finalversion get_latest_of_two $lastversion $finalversion lastversion else get_latest_of_two $lastversion $__current lastversion fi fi done ;; esac if git rev-parse -q --verify "refs/tags/$lastversion" >/dev/null; then hasversiontag="true" else hasversiontag="false" fi } # Gets the next version given the provided scope # $1 The version that is going to be bumped # $2 The scope to bump # $3 The variable where to stoer the result function get_next_version { local __exploded local __fromversion=$1 local __scope=$2 local __result=$3 explode_version $__fromversion __exploded case $__scope in major) __exploded[0]=$((${__exploded[0]}+1)) __exploded[1]=0 __exploded[2]=0 ;; minor) __exploded[1]=$((${__exploded[1]}+1)) __exploded[2]=0 ;; patch) __exploded[2]=$((${__exploded[2]}+1)) ;; esac eval "$__result=${prefix}${__exploded[0]}.${__exploded[1]}.${__exploded[2]}" } function bump_version { ## First we try to get the next version based on the existing last one if [ "$scope" == "auto" ]; then get_scope_auto scope fi local __candidatefromlast=$FIRST_VERSION local __explodedlast explode_version $lastversion __explodedlast if [[ -n "${__explodedlast[3]}" ]]; then # Last version is not final local __idlast explode_identifier ${__explodedlast[3]} __idlast # We get the last, given the desired id based on the scope __candidatefromlast="${prefix}${__explodedlast[0]}.${__explodedlast[1]}.${__explodedlast[2]}" if [[ -n "$identifier" ]]; then local __nextid="$identifier.1" if [ "$identifier" == "${__idlast[0]}" ]; then # We target the same identifier as the last so we increase one __nextid="$identifier.$(( ${__idlast[1]}+1 ))" __candidatefromlast="$__candidatefromlast-$__nextid" else # Different identifiers, we make sure we are assigning a higher identifier, if not, we increase the version __candidatefromlast="$__candidatefromlast-$__nextid" local __comparedwithlast compare_versions $__candidatefromlast $lastversion __comparedwithlast if [ "$__comparedwithlast" == -1 ]; then get_next_version $__candidatefromlast $scope __candidatefromlast __candidatefromlast="$__candidatefromlast-$__nextid" fi fi fi fi # Then we try to get the version based on the latest final one local __candidatefromfinal=$FIRST_VERSION get_next_version $finalversion $scope __candidatefromfinal if [[ -n "$identifier" ]]; then __candidatefromfinal="$__candidatefromfinal-$identifier.1" fi # Finally we compare both candidates local __resultversion local __result compare_versions $__candidatefromlast $__candidatefromfinal __result case $__result in 0) __resultversion=$__candidatefromlast ;; -1) __resultversion="$__candidatefromfinal" ;; 1) __resultversion=$__candidatefromlast ;; esac eval "$1=$__resultversion" } function increase_version { local __version= if [ -z $forcedversion ]; then bump_version __version else if [[ $forcedversion =~ $SEMVER_REGEX ]] ; then compare_versions $forcedversion $lastversion __result if [ $__result -le 0 ]; then echo "Version can't be lower than last version: $lastversion" exit 1 fi else echo "Non valid version to bump" exit 1 fi __version=$forcedversion fi if [ "$displayonly" == "true" ]; then echo "$__version" else if [ "$forcetag" == "false" ]; then check_git_dirty_status fi local __commitlist if [ "$finalversion" == "$FIRST_VERSION" ] || [ "$hasversiontag" != "true" ]; then __commitlist="$(git log --pretty=oneline | cat)" else __commitlist="$(git log --pretty=oneline $finalversion... | cat)" fi # If we are forcing a bump, we add bump to the commit list if [[ -z $__commitlist && "$forcetag" == "true" ]]; then __commitlist="bump" fi if [[ -z $__commitlist ]]; then echo "No commits since the last final version, not bumping version" else if [[ -z $versionname ]]; then versionname=$(date -u +"%Y-%m-%dT%H:%M:%SZ") fi local __message="$versionname $__commitlist" # We check we have info on the user local __username=$(git config user.name) if [ -z "$__username" ]; then __username=$(id -u -n) git config user.name $__username fi local __useremail=$(git config user.email) if [ -z "$__useremail" ]; then __useremail=$(hostname) git config user.email "$__username@$__useremail" fi git tag -a $__version -m "$__message" # If we have a remote, we push there local __remotes=$(git remote) if [[ -n $__remotes ]]; then for __remote in $__remotes; do git push $__remote $__version > /dev/null if [ $? -eq 0 ]; then echo "$__version pushed to $__remote" else echo "Error pushing the tag $__version to $__remote" exit 1 fi done else echo "$__version" fi fi fi } function check_git_dirty_status { local __repostatus= get_work_tree_status __repostatus if [ "$__repostatus" == "uncommitted" ]; then echo "ERROR: You have uncommitted changes" git status --porcelain exit 1 fi if [ "$__repostatus" == "unstaged" ]; then echo "ERROR: You have unstaged changes" git status --porcelain exit 1 fi } # Get the total amount of lines of code in the repo function get_total_lines { local __empty_id="$(git hash-object -t tree /dev/null)" local __changes="$(git diff --numstat $__empty_id | cat)" local __added_deleted=$1 get_changed_lines "$__changes" $__added_deleted } # Get the total amount of lines of code since the provided tag function get_sincetag_lines { local __sincetag=$1 local __changes="$(git diff --numstat $__sincetag | cat)" local __added_deleted=$2 get_changed_lines "$__changes" $__added_deleted } function get_changed_lines { local __changes_numstat=$1 local __result=$2 IFS=$'\n' read -rd '' -a __changes_array <<<"$__changes_numstat" local __diff_regex="^([0-9]+)[[:space:]]+([0-9]+)[[:space:]]+.+$" local __total_added=0 local __total_deleted=0 for i in "${__changes_array[@]}" do if [[ $i =~ $__diff_regex ]] ; then local __added=${BASH_REMATCH[1]} local __deleted=${BASH_REMATCH[2]} __total_added=$(( $__total_added+$__added )) __total_deleted=$(( $__total_deleted+$__deleted )) fi done eval "$2=( $__total_added $__total_deleted )" } function get_scope_auto { local __verbose=$2 local __total=0 local __since=0 local __scope= get_total_lines __total get_sincetag_lines $finalversion __since local __percentage=0 if [ "$__total" != "0" ]; then local __percentage=$(( 100*$__since/$__total )) if [ $__percentage -gt "10" ]; then __scope="minor" else __scope="patch" fi fi eval "$1=$__scope" if [[ -n "$__verbose" ]]; then echo "[Auto Scope] Percentage of lines changed: $__percentage" echo "[Auto Scope] : $__scope" fi } function get_work_tree_status { # Update the index git update-index -q --ignore-submodules --refresh > /dev/null eval "$1=" if ! git diff-files --quiet --ignore-submodules -- > /dev/null then eval "$1=unstaged" fi if ! git diff-index --cached --quiet HEAD --ignore-submodules -- > /dev/null then eval "$1=uncommitted" fi } function get_current { if [ "$hasversiontag" == "true" ]; then local __commitcount="$(git rev-list $lastversion.. --count)" else local __commitcount="$(git rev-list --count HEAD)" fi local __status= get_work_tree_status __status if [ "$__commitcount" == "0" ] && [ -z "$__status" ]; then eval "$1=$lastversion" else local __buildinfo="$(git rev-parse --short HEAD)" local __currentbranch="$(git rev-parse --abbrev-ref HEAD | cut -c1-$MAX_BRANCH_LENGTH)" get_default_branch default_branch if [ "$__currentbranch" != "master" ]; then __buildinfo="$__currentbranch.$__buildinfo" fi local __suffix= if [ "$__commitcount" != "0" ]; then if [ -n "$__suffix" ]; then __suffix="$__suffix." fi __suffix="$__suffix$__commitcount" fi if [ -n "$__status" ]; then if [ -n "$__suffix" ]; then __suffix="$__suffix." fi __suffix="$__suffix$__status" fi __suffix="$__suffix+$__buildinfo" if [ "$lastversion" == "$finalversion" ]; then scope="patch" identifier= local __bumped= bump_version __bumped eval "$1=$__bumped-dev.$__suffix" else eval "$1=$lastversion.$__suffix" fi fi } function init { TAGS="$(git tag --merged)" IFS=$'\n' read -rd '' -a TAG_ARRAY <<<"$TAGS" get_latest ${TAG_ARRAY[@]} currentbranch="$(git rev-parse --abbrev-ref HEAD)" } case $ACTION in --help) echo -e "$HELP" ;; --version) echo -e "${PROG}: $PROG_VERSION" ;; final) init get_default_branch default_branch diff=$(git diff $default_branch | cat) if [ "$forcetag" == "false" ]; then if [ -n "$diff" ]; then echo "ERROR: Branch must be updated with $default_branch for final versions" exit 1 fi fi increase_version ;; alpha|beta) init identifier="$ACTION" increase_version ;; candidate) init identifier="rc" increase_version ;; getlast) init echo "$lastversion" ;; getfinal) init echo "$finalversion" ;; getcurrent) init get_current current echo "$current" ;; get) init echo "Current final version: $finalversion" echo "Last tagged version: $lastversion" ;; *) echo "'$ACTION' is not a valid command, see --help for available commands." ;; esac ================================================ FILE: test_semtag.sh ================================================ #!/usr/bin/env bash # Test script for semtag beta/alpha/rc increment functionality # This script validates that pre-release version increments work correctly set -euo pipefail SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" SEMTAG="$SCRIPT_DIR/semtag" # Colors for output RED='\033[0;31m' GREEN='\033[0;32m' YELLOW='\033[1;33m' NC='\033[0m' # No Color # Test counters TESTS_RUN=0 TESTS_PASSED=0 TESTS_FAILED=0 # Array to store created tags for cleanup CREATED_TAGS=() # Function to print test results print_result() { local test_name="$1" local expected="$2" local actual="$3" TESTS_RUN=$((TESTS_RUN + 1)) if [ "$expected" = "$actual" ]; then echo -e "${GREEN}✓${NC} $test_name" echo " Expected: $expected" echo " Got: $actual" TESTS_PASSED=$((TESTS_PASSED + 1)) else echo -e "${RED}✗${NC} $test_name" echo " Expected: $expected" echo " Got: $actual" TESTS_FAILED=$((TESTS_FAILED + 1)) fi echo } # Function to create a test tag create_tag() { local tag="$1" git tag "$tag" -m "test tag: $tag" >/dev/null 2>&1 CREATED_TAGS+=("$tag") } # Function to delete test tags cleanup_tags() { if [ ${#CREATED_TAGS[@]} -gt 0 ]; then echo -e "${YELLOW}Cleaning up test tags...${NC}" for tag in "${CREATED_TAGS[@]}"; do git tag -d "$tag" >/dev/null 2>&1 || true done CREATED_TAGS=() fi } # Cleanup on exit trap cleanup_tags EXIT echo "================================================" echo "Semtag Pre-release Version Increment Tests" echo "================================================" echo # Get the current state INITIAL_LAST=$("$SEMTAG" getlast) INITIAL_FINAL=$("$SEMTAG" getfinal) echo "Initial state:" echo " Last version: $INITIAL_LAST" echo " Final version: $INITIAL_FINAL" echo # Test 1: Beta with patch scope - first beta echo "=== Test 1: Beta Patch Increment ===" result=$("$SEMTAG" beta -s patch -o) print_result "First beta with patch scope" "v0.2.1-beta.1" "$result" # Test 2: Beta with patch scope - increment beta number create_tag "v0.2.1-beta.1" result=$("$SEMTAG" beta -s patch -o) print_result "Increment beta.1 to beta.2" "v0.2.1-beta.2" "$result" # Test 3: Beta with patch scope - increment beta number again create_tag "v0.2.1-beta.2" result=$("$SEMTAG" beta -s patch -o) print_result "Increment beta.2 to beta.3" "v0.2.1-beta.3" "$result" cleanup_tags # Test 4: Beta with minor scope - first beta echo "=== Test 2: Beta Minor Increment ===" result=$("$SEMTAG" beta -s minor -o) print_result "First beta with minor scope" "v0.3.0-beta.1" "$result" # Test 5: Beta with minor scope - increment beta number create_tag "v0.3.0-beta.1" result=$("$SEMTAG" beta -s minor -o) print_result "Increment beta.1 to beta.2 (minor)" "v0.3.0-beta.2" "$result" # Test 6: Beta with minor scope - increment beta number again create_tag "v0.3.0-beta.2" result=$("$SEMTAG" beta -s minor -o) print_result "Increment beta.2 to beta.3 (minor)" "v0.3.0-beta.3" "$result" cleanup_tags # Test 7: Beta with major scope - first beta echo "=== Test 3: Beta Major Increment ===" result=$("$SEMTAG" beta -s major -o) print_result "First beta with major scope" "v1.0.0-beta.1" "$result" # Test 8: Beta with major scope - increment beta number create_tag "v1.0.0-beta.1" result=$("$SEMTAG" beta -s major -o) print_result "Increment beta.1 to beta.2 (major)" "v1.0.0-beta.2" "$result" cleanup_tags # Test 9: Alpha increments echo "=== Test 4: Alpha Increment ===" result=$("$SEMTAG" alpha -s patch -o) print_result "First alpha with patch scope" "v0.2.1-alpha.1" "$result" create_tag "v0.2.1-alpha.1" result=$("$SEMTAG" alpha -s patch -o) print_result "Increment alpha.1 to alpha.2" "v0.2.1-alpha.2" "$result" cleanup_tags # Test 10: Release candidate increments echo "=== Test 5: Release Candidate Increment ===" result=$("$SEMTAG" candidate -s patch -o) print_result "First rc with patch scope" "v0.2.1-rc.1" "$result" create_tag "v0.2.1-rc.1" result=$("$SEMTAG" candidate -s patch -o) print_result "Increment rc.1 to rc.2" "v0.2.1-rc.2" "$result" cleanup_tags # Test 11: Switching between identifiers echo "=== Test 6: Identifier Switching ===" create_tag "v0.3.0-alpha.5" result=$("$SEMTAG" beta -s minor -o) print_result "Switch from alpha to beta (same version)" "v0.3.0-beta.1" "$result" create_tag "v0.3.0-beta.1" result=$("$SEMTAG" alpha -s minor -o) print_result "Switch from beta to alpha (bumps version)" "v0.4.0-alpha.1" "$result" cleanup_tags # Test 12: Multiple sequential increments echo "=== Test 7: Sequential Beta Increments ===" create_tag "v0.2.1-beta.1" create_tag "v0.2.1-beta.2" create_tag "v0.2.1-beta.3" result=$("$SEMTAG" beta -s patch -o) print_result "Increment beta.3 to beta.4" "v0.2.1-beta.4" "$result" cleanup_tags # Test 13: Final version with patch scope echo "=== Test 8: Final Version Patch Increment ===" # Start from v0.2.0, increment patch result=$("$SEMTAG" final -s patch -o -f) print_result "Final version patch increment" "v0.2.1" "$result" # Test 14: Final version with minor scope echo "=== Test 9: Final Version Minor Increment ===" result=$("$SEMTAG" final -s minor -o -f) print_result "Final version minor increment" "v0.3.0" "$result" # Test 15: Final version with major scope echo "=== Test 10: Final Version Major Increment ===" result=$("$SEMTAG" final -s major -o -f) print_result "Final version major increment" "v1.0.0" "$result" # Test 16: Sequential final versions echo "=== Test 11: Sequential Final Version Increments ===" create_tag "v0.2.1" result=$("$SEMTAG" final -s patch -o -f) print_result "Increment v0.2.1 to v0.2.2" "v0.2.2" "$result" create_tag "v0.2.2" result=$("$SEMTAG" final -s patch -o -f) print_result "Increment v0.2.2 to v0.2.3" "v0.2.3" "$result" cleanup_tags # Test 17: Going from pre-release to final echo "=== Test 12: Pre-release to Final Version ===" create_tag "v0.3.0-beta.3" result=$("$SEMTAG" final -s minor -o -f) print_result "From beta.3 to final minor version" "v0.3.0" "$result" create_tag "v0.4.0-rc.2" result=$("$SEMTAG" final -s minor -o -f) print_result "From rc.2 to final minor version" "v0.4.0" "$result" cleanup_tags # Test 18: Mixed major/minor/patch with pre-releases echo "=== Test 13: Mixed Version Scenarios ===" create_tag "v1.5.3" result=$("$SEMTAG" beta -s patch -o) print_result "From v1.5.3 to beta patch" "v1.5.4-beta.1" "$result" create_tag "v1.5.4-beta.1" create_tag "v1.5.4-beta.2" result=$("$SEMTAG" final -s patch -o -f) print_result "From beta.2 to final patch" "v1.5.4" "$result" cleanup_tags # Test 19: Major version with pre-releases echo "=== Test 14: Major Version with Pre-releases ===" create_tag "v2.0.0" result=$("$SEMTAG" beta -s major -o) print_result "From v2.0.0 to major beta" "v3.0.0-beta.1" "$result" create_tag "v3.0.0-beta.1" result=$("$SEMTAG" final -s major -o -f) print_result "From beta.1 to final major" "v3.0.0" "$result" cleanup_tags # Print summary echo "================================================" echo "Test Summary" echo "================================================" echo -e "Tests run: $TESTS_RUN" echo -e "${GREEN}Tests passed: $TESTS_PASSED${NC}" if [ $TESTS_FAILED -gt 0 ]; then echo -e "${RED}Tests failed: $TESTS_FAILED${NC}" else echo -e "${GREEN}Tests failed: $TESTS_FAILED${NC}" fi echo "================================================" # Exit with failure if any tests failed if [ $TESTS_FAILED -gt 0 ]; then exit 1 else echo -e "${GREEN}All tests passed!${NC}" exit 0 fi