How to pass a command line argument to an executable script in a shared library - git tag / versioning

Hello all,
I was recently tasked with pushing git tags using semantic versioning when a successful build is completed on our main or release branch.
I wasn’t sure how to accomplish this task so I came up with the following solution:

  • Enable Jenkins multibranch pipeline to see new tags and build when a new tag is pushed - Seems to work fine when I execute the script manually. I see the new git tag with the appropriate version in my Jenkins multibranch pipeline job, my repo gets the tag and everything appears to be good.

  • Now I’d like to automate the whole release process by placing the executable script in the Jenkins shared library, which I have done. - Shared library is working fine, and my Jenkinsfile is able to see the executable script.

My problem is I’m not sure how I would I pass a command line argument to the git tag script in my shared library? I need to be able to tell the script if its a major, minor, or patch release.

I’m not 100% sure this is the best solution… So, if anyone has any feedback on how to accomplish this better please, please, please share. Any suggestions are greatly appreciated…

Thanks all,

For testing purposes I have setup the following:
Jenkinsfile

@Library('shared-library') _
//
pipeline {
    agent any
    stages {
        stage('tagging') {
            steps {
                script {
                    tagRelease()
                }
            }
        }
    }
}

executable git-tagger script

#!/bin/bash
ARGUMENTS="$@"
BRANCH="main"
DRYRUN="0"
GITPARAMS=()
RELEASEDATE=$(date '+%Y%m%d')
RELEASENOTES=""
REMOTE="origin"
PREVIOUS_COMMIT=""
RUNSILENT="0"
VERBOSE="0"
VERSIONTYPE="patch"


# Function to return help text when requested with -h option.
help() {
    # Display help.
    echo
    echo "Generates, increments and pushes an annotated semantic version tag for current git repository."
    echo
    echo "Usage:"
    echo "  sh tag-release [-b|--branch] [-d|--date] [-h|--help] [-m|--message] [-p|--previous] [-r|--remote] [-v|--version-type]"
    echo
    echo "Options:"
    echo "-b,--branch <branch>"
    echo "      Branch to generate release tag on."
    echo "      If ommited, defaults to '$BRANCH'"
    echo
    echo "-d,--date <date>"
    echo "      Date string to specify when release was created."
    echo "      If ommited, defaults to %Y%m%d of current date."
    echo
    echo "-h,--help, help"
    echo "      Prints this help."
    echo
    echo "-m,--message <message>"
    echo "      Message to use to annotate release."
    echo "      If ommited a list of non-merge commit messages will be compiled as release annotation."
    echo "      If ommited and -p <commit> is given, will compile a list of non-merge commit messages between <commit> and HEAD."
    echo "      If ommited and -p is not given, will compile a list of non-merge commit messages between last found release and HEAD."
    echo
    echo "-n,--dry-run"
    echo "      Do everything except actually send the updates."
    echo "      If -q is also given then only error messages will be output."
    echo
    echo "-p,--previous <commit>"
    echo "      Previous commit to use to generate release notes."
    echo "      If ommited, will attempt to get commit hash of last release tag."
    echo
    echo "-q,--quiet"
    echo "      Supress all output, unless an error occurs."
    echo
    echo "-r,--remote <remote>"
    echo "      Name of remote to use for pushing."
    echo "      If ommited, defaults to '$REMOTE'"
    echo
    echo "-t,--version-type [major|minor|patch]"
    echo "      Type of semantic version to create. Valid options are 'major', 'minor' or 'patch'"
    echo "          major: Will bump up to next major release (i.e 1.0.0 -> 2.0.0)"
    echo "          minor: Will bump up to next minor release (i.e 1.0.1 -> 1.1.0)"
    echo "          patch: Will bump up to next patch release (i.e 1.0.2 -> 1.0.3)"
    echo "      If ommited, will default to '$VERSIONTYPE'"
    echo
    echo "-v,--verbose"
    echo "      Run verbosley."
    echo
}

conditional_echo() {
    if [[ "$RUNQUIET" -eq 0 ]]; then
        echo "$1"
    fi
}

while [[ "$#" -gt 0 ]]
do
    case $1 in
      -b|--branch)
        BRANCH=$2
        ;;
      -d|--date)
        RELEASEDATE=$2
        ;;
      -h|--help|help)
        help
        exit
        ;;
      -m|--message)
        RELEASENOTES=$2
        ;;
      -n|--dry-run)
        DRYRUN="1"
        ;;
      -p|--previous)
        PREVIOUS_COMMIT=$2
        ;;
      -r|--remote)
        REMOTE=$2
        ;;
      -t|--type)
        VERSIONTYPE=$2
        ;;
      -q|--quiet)
        RUNQUIET="1"
        GITPARAMS+=(--dry-run)
        ;;
      -v|--verbose)
        # See if a second argument is passed, due to argument reassignment to -v.
        if [[ "$1" == "-v" ]] && [[ -n "$2" ]]; then
            echo "ERROR: Unsupported value \"$2\" passed to -v argument. If trying to set semantic version tag, use the -t or --type argument".
            exit
        fi
        VERBOSE="1"
        GITPARAMS+=(--verbose)
        ;;
    esac
    shift
done

# Get top-level of git repo.
REPO_DIR=$(echo $(git rev-parse --show-toplevel))
# CD into the top level
cd "${REPO_DIR}"

# Get current active branch
CURRENT_BRANCH=$(git rev-parse --abbrev-ref HEAD)
# Switch to production branch
if [ $CURRENT_BRANCH != "$BRANCH" ]; then
    conditional_echo "- Switching from $CURRENT_BRANCH to $BRANCH branch. (stashing any local change)"
    # stash any current work
    git stash "${GITPARAMS[@]}"
    # go to the production branch
    git checkout $BRANCH "${GITPARAMS[@]}"
fi

conditional_echo "- Updating local $BRANCH branch."
# pull latest version of production branch
git pull $REMOTE $BRANCH "${GITPARAMS[@]}"
# fetch remote, to get latest tags
git fetch $REMOTE "${GITPARAMS[@]}"

# Get previous release tags
conditional_echo "- Getting previous tag."
PREVIOUS_TAG=$(echo $(git ls-remote --tags --ref --sort="v:refname" $REMOTE | tail -n1))

# If specific commit not set, get the from the previous release.
if [ -z "$PREVIOUS_COMMIT" ]; then
    # Split on the first space
    PREVIOUS_COMMIT=$(echo $PREVIOUS_TAG | cut -d' ' -f 1)
fi

conditional_echo "-- PREVIOUS TAG: $PREVIOUS_TAG"

# Get previous release number
PREVIOUS_RELEASE=$(echo $PREVIOUS_TAG | cut -d'/' -f 3 | cut -d'v' -f2 )

conditional_echo "- Creating release tag"
# Get last commit
LASTCOMMIT=$(echo $(git rev-parse $REMOTE/$BRANCH))
# Check if commit already has a tag
NEEDSTAG=$(echo $(git describe --contains $LASTCOMMIT 2>/dev/null))

if [ -z "$NEEDSTAG" ]; then
    conditional_echo "-- Generating release number ($VERSIONTYPE)"
    # Replace . with spaces so that can split into an array.
    VERSION_BITS=(${PREVIOUS_RELEASE//./ })
    # Get number parts, only the digits.
    VNUM1=${VERSION_BITS[0]//[^0-9]/}
    VNUM2=${VERSION_BITS[1]//[^[0-9]/}
    VNUM3=${VERSION_BITS[2]//[^0-9]/}
    # Update tagging number based on option that was passed.
    if [ "$VERSIONTYPE" == "major" ]; then
        VNUM1=$((VNUM1+1))
        VNUM2=0
        VNUM3=0
    elif [ "$VERSIONTYPE" == "minor" ]; then
        VNUM2=$((VNUM2+1))
        VNUM3=0
    else
        # Assume TAGTYPE = "patch"
        VNUM3=$((VNUM3+1))
    fi

    # Create new tag number
    NEWTAG="$BRANCH-v$VNUM1.$VNUM2.$VNUM3"
    conditional_echo "-- Release number: $NEWTAG"
    # Check to see if new tag already exists
    TAGEXISTS=$(echo $(git ls-remote --tags --ref $REMOTE | grep "$NEWTAG"))

    if [ -z "$TAGEXISTS" ]; then
        # Check if release notes were not provided.
        if [ -z "$RELEASENOTES" ]; then
            conditional_echo "- Generating basic release notes of commits since last release."
            # Generate a list of commit messages since the last release.
            RELEASENOTES=$(git log --pretty=format:"- %s" $PREVIOUS_COMMIT...$LASTCOMMIT  --no-merges)
        fi
        # Tag the commit.
        if [[ "$DRYRUN" -eq 0 ]]; then
            conditional_echo "-- Tagging commit. ($LASTCOMMIT)"
            git tag -a $NEWTAG -m"$RELEASEDATE: Release $VNUM1.$VNUM2.$VNUM3" -m"$RELEASENOTES" $LASTCOMMIT
            conditional_echo "- Pushing release to $REMOTE"
            # Push up the tag
            git push $REMOTE $NEWTAG "${GITPARAMS[@]}"
        else
            conditional_echo "Release Notes:"
            conditional_echo "$RELEASENOTES"
        fi
    else
        conditional_echo "-- ERROR: TAG $NEWTAG already exists."
        exit 1
    fi
else
    conditional_echo "-- ERROR: Commit already tagged as a release. ($LASTCOMMIT)"
    exit 1
fi

# Switch to back to original branch
if [ $CURRENT_BRANCH != "$BRANCH" ]; then
    conditional_echo "- Switching back to $CURRENT_BRANCH branch. (restoring local changes)"
    git checkout "$CURRENT_BRANCH" "${GITPARAMS[@]}"
    # remove the stash
    git stash pop "${GITPARAMS[@]}"
fi

exit 0

in your shared library, you have a def call(), you can give a list of parameters, so you could have def call(major, minor, patch) or def call(arguments) or something

then later you wrap your call in withEnv (for example if you use sh which i’m guessing from the git-tagger script)

withEnv(["MAJOR=$major"]) {
  sh('./git-tagger "${MAJOR} ")
}

or however your script works (note the double quotes for withEnv, so groovy can interpolate the $major, and single quotes for sh, so it passes it to base as is.

Halkeye, as always your on it!!! I super appreciate you taking the time to answer my question.
Thanks dude!
Johnny M

Halkeye, your suggestion above worked great, thank you. I’m wondering if I can bounce one last question off of you?

I have been trying to troubleshoot an error message when calling my tagging script…
Stacktrace

groovy.lang.MissingPropertyException: No such property: major for class: groovy.lang.Binding
	at groovy.lang.Binding.getVariable(Binding.java:63)
	at org.jenkinsci.plugins.scriptsecurity.sandbox.groovy.SandboxInterceptor.onGetProperty(SandboxInterceptor.java:266)
	at org.kohsuke.groovy.sandbox.impl.Checker$7.call(Checker.java:375)
	at org.kohsuke.groovy.sandbox.impl.Checker.checkedGetProperty(Checker.java:379)
	at org.kohsuke.groovy.sandbox.impl.Checker.checkedGetProperty(Checker.java:355)
	at org.kohsuke.groovy.sandbox.impl.Checker.checkedGetProperty(Checker.java:355)
	at com.cloudbees.groovy.cps.sandbox.SandboxInvoker.getProperty(SandboxInvoker.java:29)
	at com.cloudbees.groovy.cps.impl.PropertyAccessBlock.rawGet(PropertyAccessBlock.java:20)
	at WorkflowScript.run(WorkflowScript:8)
	at org.jenkinsci.plugins.pipeline.modeldefinition.ModelInterpreter.delegateAndExecute(ModelInterpreter.groovy:137)
	at org.jenkinsci.plugins.pipeline.modeldefinition.ModelInterpreter.executeSingleStage(ModelInterpreter.groovy:666)
	at org.jenkinsci.plugins.pipeline.modeldefinition.ModelInterpreter.catchRequiredContextForNode(ModelInterpreter.groovy:395)
	at org.jenkinsci.plugins.pipeline.modeldefinition.ModelInterpreter.catchRequiredContextForNode(ModelInterpreter.groovy:393)
	at org.jenkinsci.plugins.pipeline.modeldefinition.ModelInterpreter.executeSingleStage(ModelInterpreter.groovy:665)
	at org.jenkinsci.plugins.pipeline.modeldefinition.ModelInterpreter.evaluateStage(ModelInterpreter.groovy:288)
	at org.jenkinsci.plugins.pipeline.modeldefinition.ModelInterpreter.toolsBlock(ModelInterpreter.groovy:544)
	at org.jenkinsci.plugins.pipeline.modeldefinition.ModelInterpreter.toolsBlock(ModelInterpreter.groovy:543)
	at org.jenkinsci.plugins.pipeline.modeldefinition.ModelInterpreter.evaluateStage(ModelInterpreter.groovy:276)
	at org.jenkinsci.plugins.pipeline.modeldefinition.ModelInterpreter.withEnvBlock(ModelInterpreter.groovy:443)
	at org.jenkinsci.plugins.pipeline.modeldefinition.ModelInterpreter.withEnvBlock(ModelInterpreter.groovy:442)
	at org.jenkinsci.plugins.pipeline.modeldefinition.ModelInterpreter.evaluateStage(ModelInterpreter.groovy:275)
	at org.jenkinsci.plugins.pipeline.modeldefinition.ModelInterpreter.withCredentialsBlock(ModelInterpreter.groovy:481)
	at org.jenkinsci.plugins.pipeline.modeldefinition.ModelInterpreter.withCredentialsBlock(ModelInterpreter.groovy:480)
	at org.jenkinsci.plugins.pipeline.modeldefinition.ModelInterpreter.evaluateStage(ModelInterpreter.groovy:274)
	at org.jenkinsci.plugins.pipeline.modeldefinition.ModelInterpreter.inDeclarativeAgent(ModelInterpreter.groovy:586)
	at org.jenkinsci.plugins.pipeline.modeldefinition.ModelInterpreter.inDeclarativeAgent(ModelInterpreter.groovy:585)
	at org.jenkinsci.plugins.pipeline.modeldefinition.ModelInterpreter.evaluateStage(ModelInterpreter.groovy:272)
	at org.jenkinsci.plugins.pipeline.modeldefinition.ModelInterpreter.stageInput(ModelInterpreter.groovy:356)
	at org.jenkinsci.plugins.pipeline.modeldefinition.ModelInterpreter.stageInput(ModelInterpreter.groovy:355)
	at org.jenkinsci.plugins.pipeline.modeldefinition.ModelInterpreter.evaluateStage(ModelInterpreter.groovy:261)
	at org.jenkinsci.plugins.pipeline.modeldefinition.ModelInterpreter.inWrappers(ModelInterpreter.groovy:618)
	at org.jenkinsci.plugins.pipeline.modeldefinition.ModelInterpreter.inWrappers(ModelInterpreter.groovy:617)
	at org.jenkinsci.plugins.pipeline.modeldefinition.ModelInterpreter.evaluateStage(ModelInterpreter.groovy:259)
	at org.jenkinsci.plugins.pipeline.modeldefinition.ModelInterpreter.withEnvBlock(ModelInterpreter.groovy:443)
	at org.jenkinsci.plugins.pipeline.modeldefinition.ModelInterpreter.withEnvBlock(ModelInterpreter.groovy:442)
	at org.jenkinsci.plugins.pipeline.modeldefinition.ModelInterpreter.evaluateStage(ModelInterpreter.groovy:254)
	at ___cps.transform___(Native Method)
	at com.cloudbees.groovy.cps.impl.PropertyishBlock$ContinuationImpl.get(PropertyishBlock.java:73)
	at com.cloudbees.groovy.cps.LValueBlock$GetAdapter.receive(LValueBlock.java:30)
	at com.cloudbees.groovy.cps.impl.PropertyishBlock$ContinuationImpl.fixName(PropertyishBlock.java:65)
	at jdk.internal.reflect.GeneratedMethodAccessor327.invoke(Unknown Source)
	at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
	at java.base/java.lang.reflect.Method.invoke(Method.java:566)
	at com.cloudbees.groovy.cps.impl.ContinuationPtr$ContinuationImpl.receive(ContinuationPtr.java:72)
	at com.cloudbees.groovy.cps.impl.ConstantBlock.eval(ConstantBlock.java:21)
	at com.cloudbees.groovy.cps.Next.step(Next.java:83)
	at com.cloudbees.groovy.cps.Continuable$1.call(Continuable.java:152)
	at com.cloudbees.groovy.cps.Continuable$1.call(Continuable.java:146)
	at org.codehaus.groovy.runtime.GroovyCategorySupport$ThreadCategoryInfo.use(GroovyCategorySupport.java:136)
	at org.codehaus.groovy.runtime.GroovyCategorySupport.use(GroovyCategorySupport.java:275)
	at com.cloudbees.groovy.cps.Continuable.run0(Continuable.java:146)
	at org.jenkinsci.plugins.workflow.cps.SandboxContinuable.access$001(SandboxContinuable.java:18)
	at org.jenkinsci.plugins.workflow.cps.SandboxContinuable.run0(SandboxContinuable.java:51)
	at org.jenkinsci.plugins.workflow.cps.CpsThread.runNextChunk(CpsThread.java:187)
	at org.jenkinsci.plugins.workflow.cps.CpsThreadGroup.run(CpsThreadGroup.java:420)
	at org.jenkinsci.plugins.workflow.cps.CpsThreadGroup$2.call(CpsThreadGroup.java:330)
	at org.jenkinsci.plugins.workflow.cps.CpsThreadGroup$2.call(CpsThreadGroup.java:294)
	at org.jenkinsci.plugins.workflow.cps.CpsVmExecutorService$2.call(CpsVmExecutorService.java:67)
	at java.base/java.util.concurrent.FutureTask.run(FutureTask.java:264)
	at hudson.remoting.SingleLaneExecutorService$1.run(SingleLaneExecutorService.java:139)
	at jenkins.util.ContextResettingExecutorService$1.run(ContextResettingExecutorService.java:30)
	at jenkins.security.ImpersonatingExecutorService$1.run(ImpersonatingExecutorService.java:70)
	at java.base/java.util.concurrent.Executors$RunnableAdapter.call(Executors.java:515)
	at java.base/java.util.concurrent.FutureTask.run(FutureTask.java:264)
	at java.base/java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1128)
	at java.base/java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:628)
	at java.base/java.lang.Thread.run(Thread.java:829)

and as I understand, the error is telling me “major” isn’t declared or is unknown. I guess I’m confused as its a parameter the script takes in. I have tried a combination of things to try and fix it… such as, adding the -t flag within the jenkinsfile like so, but I get the same error as above. Im wondering if you may have a suggestion??? In the meantime I will keep looking to see what I can find.
Thanks man,
jenkinsfile

@Library('shared-library') _

pipeline {
    agent any
    stages {
        stage('tagging') {
            steps {
                withEnv(["MAJOR=$major"]) {
                    sh('./git-tagger -t "${MAJOR}"')
                }
            }
        }
    }
}

To be clear, I also tried your suggestion:

@Library('shared-library') _

pipeline {
    agent any
    stages {
        stage('tagging') {
            steps {
                withEnv(["MAJOR=$major"]) {
                    sh('./git-tagger "${MAJOR}"')
                }
            }
        }
    }
}