Auto-increment NPM package version in Azure Build Pipelines

Luke Pammant
6 min readSep 17, 2020
azure-pipeline.yml

tl;dr: use this gist for a for the full pipeline to iterate the patch version number, npm install, & npm publish your package. Or simply just use the Bash task to modify the package.json before the publish task

I was following this article to auto increment the npm patch version when publishing to an Azure Artifacts NPM feed via Azure Pipelines but ran into a couple issues. Given that it was written a year ago I’ve decided to write a post about my modifications. I made gist here that you can use to follow along with.

Disclaimer: I am a build pipeline novice so please correct me if I make any major mistakes. Also I break down the bash script assuming you’re a bash novice. If you already know what jq, curl, and sed do just use the gist

The first difference is that I use the version from my package.json file. This will allow the developer to increment the major and minor versions via code and the patch number will be auto-incremented via the bash script in the azure-pipeline.yml.

// package.json
{
“name”: “@myCompany/shared-component-library”,
“version”: “0.0.0”, /*major.minor.patch(auto-incremented)*/
// ...
}

Next I had to change several pieces of the bash script and found some places where we can reduce the amount of curl requests to get version numbers. Here is the new bash script based on the one from the article I mention above:

# Get package name and version from package.json file
package_name=$(jq -r ".name" package.json)
package_version=$(jq -r ".version" package.json)
# REST API URL to get package id from npm feed
get_package_id_URL="https://feeds.dev.azure.com/<yourCompanyId>/_apis/packaging/Feeds/<yourFeedId>/packages?protocolType=Npm&packageNameQuery=$package_name"
# next, let's get all available versions for our package
all_versions_URL=$(curl -s -X GET -u PATUSER:$SYSTEM_ACCESSTOKEN $get_package_id_URL | jq -r '.value[0]._links.versions.href')
all_versions=$(curl -s -X GET -u PATUSER:$SYSTEM_ACCESSTOKEN $all_versions_URL | jq -r '.value[].version')
all=($all_versions)
echo All package verions: $all
# if we find out that the version we're trying to publish already exists in the feed, then let's increment patch version for that package and publish
if [[ " ${all[@]} " =~ " ${package_version} " ]]; then
echo Current package version found in existing packages. Iterating the patch number...
# get latest version currently published in the feed for our package
latest_version=$(curl -s -X GET -u PATUSER:$SYSTEM_ACCESSTOKEN $get_package_id_URL| jq -r '.value[].versions[].version')
IFS=. read i1 i2 i3 <<< "$latest_version"
i3_updated=$((i3 + 1))
new_version=$i1.$i2.$i3_updated
new_buildnumber=$i1.$i2.$i3_updated
echo New package version: $new_version

# update patch number variable
echo "##vso[task.setvariable variable=patch;]$i3_updated"

# update build number of the current build. let's keep things tidy
echo "##vso[build.updatebuildnumber]$new_buildnumber"
echo Replacing \"version\": \"$package_version\" with \"version\": \"$new_version\" in local package.json
sed -i 's/"version": "'${package_version}'"/"version": "'${new_version}'"/' package.json
echo new package.json version: $(jq -r ".version" package.json)
fi

The script is surprisingly simple if you know some basic bash scripting but lets break it down…

First we initialize the local variables package_name and package_version using jq to parse the package.json file and pull the the values out of the properties name and version respectively:

package_name=$(jq -r ".name" package.json)
package_version=$(jq -r ".version" package.json)

The ‘.’ in front of “name” and “version” are indicators to jq to get the properties from the root of the object. jq is used quite a lot in this script and is really helpful for parsing and extracting data from JSON formatted text.

Next we use the package_name variable to make a curl request to the NPM feed to get the versions URL, then call curl that version url to get all the currently published versions of the NPM package.

get_package_id_URL="https://feeds.dev.azure.com/<yourCompanyId>/_apis/packaging/Feeds/<yourFeedId>/packages?protocolType=Npm&packageNameQuery=$package_name"# next, let's get all available versions for our package
all_versions_URL=$(curl -s -X GET -u PATUSER:$SYSTEM_ACCESSTOKEN $get_package_id_URL | jq -r '.value[0]._links.versions.href')
all_versions=$(curl -s -X GET -u PATUSER:$SYSTEM_ACCESSTOKEN $all_versions_URL | jq -r '.value[].version')
all=($all_versions)
echo All package verions: $all

There’s a lot going on in that code snippet above. Lets take a closer look at the curl request:

all_versions_URL=$(curl -s -X GET -u PATUSER:$SYSTEM_ACCESSTOKEN $get_package_id_URL | jq -r '.value[0]._links.versions.href')

Since we are publishing to a private NPM feed in Azure Artifacts we need to pass a user via -u PATUSER:$SYSTEM_ACCESSTOKEN. The $SYSTEM_ACCESSTOKEN will be set to a “Personal Access Token” via Azure Pipelines so it can access the private repo. We do this via setting the env SYSTEM_ACCESSTOKEN: $(System.AccessToken)

The jq -r ‘.value[0]._links.versions.href’ part tells jq to drill into the first item in the value array, and get the _links.versions.href value from the response. The response looks something like this:

{
"value": [
{
// ...
"_links": {
// ...
"versions": {
"href": "https://feeds.dev.azure.com/myCompany/_apis/Packaging/Feeds/myFeed/Packages/shared-component-library/Versions"
// ...

Similarly on the next line we use that versions url to make another curl request and pull out all the versions via all_versions=$(curl -s -X GET -u PATUSER:$SYSTEM_ACCESSTOKEN $all_versions_URL | jq -r ‘.value[].version’) . Note the .value[] which tells jq to get the .version from all the elements in the value array.

{
"value": [
{
//...
"version": "0.0.1"
},
{
//...
"version": "0.0.2"
},

The all=($all_versions) just puts those versions in an array that we can use the following if statement if [[ “ ${all[@]} “ =~ “ ${package_version} “ ]];. This is basically just checking if the current version of the package is in the all array.

Most of the code in the if statement is all same song different verse so I’ll save you the technicals on that. The next interesting piece is these lines:

IFS=. read i1 i2 i3 <<< "$latest_version"
i3_updated=$((i3 + 1))

Basically what this is doing is taking the $latest_version and seperating them by the “.” character and sticking them into the variables i1 i2 and i3 (where i1 = major version, i2 = minor version, i3 = patch version). We then simply add 1 to i3 and assign it to i3_updated.

This is the part we’ve all been waiting for — actually setting the version in the package.json: sed -i ‘s/”version”: “‘${package_version}’”/”version”: “‘${new_version}’”/’ package.json. This basically uses does an inline edit (via the -i flag) to the package.json file to replace the $package_version with the $new_version. This way when we run npm publish later it will have the new package version.

So with all that out the way the bash script build step should look like this (be sure to set the env vars):

- task: Bash@3
displayName: Set NPM package patch number
env:
companyName: '<your company name - used in the npm feed url>'
feedName: '<your feed name - used in the npm feed url>'
SYSTEM_ACCESSTOKEN: $(System.AccessToken)
inputs:
targetType: 'inline'
script: |
# Get package name and version from package.json file
packageName=$(jq -r ".name" package.json)
package_version=$(jq -r ".version" package.json)
get_package_id_URL="https://feeds.dev.azure.com/${companyName}/_apis/packaging/Feeds/${feedName}/packages?protocolType=Npm&packageNameQuery=$packageName"
# next, let's get all available versions for our package
all_versions_URL=$(curl -s -X GET -u PATUSER:$SYSTEM_ACCESSTOKEN $get_package_id_URL | jq -r '.value[0]._links.versions.href')
all_versions=$(curl -s -X GET -u PATUSER:$SYSTEM_ACCESSTOKEN $all_versions_URL | jq -r '.value[].version')
all=($all_versions)

# if we find out that the version we're trying to publish already exists in the feed, then let's increment patch version for that package and publish
if [[ " ${all[@]} " =~ " ${package_version} " ]]; then
echo Current package version found in existing packages. Iterating the patch number...
# get latest version currently published in the feed for our package
latest_version=$(curl -s -X GET -u PATUSER:$SYSTEM_ACCESSTOKEN $get_package_id_URL| jq -r '.value[].versions[].version')
IFS=. read i1 i2 i3 <<< "$latest_version"
i3_updated=$((i3 + 1))
new_version=$i1.$i2.$i3_updated
new_buildnumber=$i1.$i2.$i3_updated
echo New package version: $new_version
# update patch number variable
echo "##vso[task.setvariable variable=patch;]$i3_updated"
# update build number of the current build. let's keep things tidy
echo "##vso[build.updatebuildnumber]$new_buildnumber"
echo Replacing \"version\": \"$package_version\" with \"version\": \"$new_version\" in local package.json
sed -i 's/"version": "'${package_version}'"/"version": "'${new_version}'"/' package.json
echo new package.json version: $(jq -r ".version" package.json)
fi

The rest of the azure-pipeline.yml file is pretty straight forward. Simply add the npm install and npm publish build steps and our package version will be reflected in Azure Artifacts.

- task: Npm@1
displayName: npm install
inputs:
command: install
- task: Npm@1
displayName: npm publish
inputs:
command: publish
publishRegistry: useFeed
publishFeed: '${feedName}'

You can reference the MS documentation on publishing to an NPM feed here if you get stuck.

Hopefully this is helpful to anyone else out there that is stuck on this.

--

--