I recently began migrating my projects from the tried-and-true “manual build and deployment” model to Azure Pipelines. For the most part, this is very straight-forward. I simply point to my csproj or package.json and say “go forth and build”. But I found that there was no native way to set version numbers in my csproj files. I didn’t want to commit versions to my repo (I’ll write a post on this in the near future), so I needed some way of automating this in Azure Pipelines.
The Azure DevOps marketplace is FILLED with solutions that would have taken care of this for me. However, I’m not an admin on our company’s DevOps account, so I couldn’t install any of them. While I could have requested an extension to be installed, I had no easy way to testing the extension first to verify it would meet my needs. So I needed another way.
Fortunately, most of my projects are web applications with an Angular front-end. This means NodeJS and NPM are already in use. So why not use them? I can write any custom script I need in JavaScript, expose the script in my package.json, then call it during the build process. This turned out to be much easier than I expected.
I started by looking for a package to parse command-line input because I needed to pass the version number to the script during the build process. I landed on yargs, a great library for parsing command-line arguments that was easy to drop in and learn on the go:
const argv = require("yargs").argv;
const appVersion = argv.appVersion;
Next, I needed a package that I could use to do a find/replace in various files in my project. The first package I stumbled upon was replace-in-file, which turned out to be just what I needed. This is a basic example of how to use it:
const argv = require("yargs").argv;
const replace = require("replace-in-file");
const appVersion = argv.appVersion;
replace.sync({
files: "./**/*.csproj",
from: /Version>\d\.\d\.\d(\.\d)?(-\w+\.?\d+)?/g,
to: appVersion
});
Now this basic example is a bit limited because it requires we use the 4-part file version format (#.#.#.#). For those of us that use SemVer, this isn’t acceptable. Further, the version passed in at the command line will be prefixed with a “v” (explained later), which needs to be stripped out. So here is the full script with those items addressed:
const argv = require("yargs").argv;
const replace = require("replace-in-file");
const appVersion = argv.appVersion;
let appVersionNoPrefix = appVersion;
if (typeof appVersion === "string" && appVersion.startsWith("v")) {
appVersionNoPrefix = appVersion.substr(1);
}
let appVersionNoPrefixNoSuffix = appVersionNoPrefix;
if (typeof appVersionNoPrefix === "string" && appVersionNoPrefix.indexOf("-") > -1) {
appVersionNoPrefixNoSuffix = appVersionNoPrefix.substring(0, appVersionNoPrefix.indexOf("-"));
}
console.log(`Setting Version to ${appVersion}...`);
const changes = replace.sync({
files: "./**/*.csproj",
from: /Version>\d\.\d\.\d(\.\d)?(-\w+\.?\d+)?/g,
to: (match) => {
if (/^Version>\d\.\d\.\d(-\w+\.?\d+)?$/.test(match)) {
return `Version>${appVersionNoPrefix}`;
}
else if (/^Version>\d\.\d\.\d\.\d(-\w+\.?\d+)?$/.test(match)) {
return `Version>${appVersionNoPrefixNoSuffix}.0`;
}
}
});
for (change of changes) {
console.log(`Successfully Set Version in ${change}.`);
}
console.log("Done");
Next, I needed to expose this script in my package.json so it could be easily called on during the build process.
{
"scripts": {
"set-version": "node ./scripts/set-version.js"
}
}
Finally, let’s wire it all up in Azure Pipelines using an NPM task.

My builds are all triggered by a tag on master, so the source branch name ends up being the version number. But you can of course use any valid build variable here.
And that’s it! I now set my version in one place – Git. Automation handles the rest.
Josh Johnson
Latest posts by Josh Johnson (see all)
- TFW Windows Interrupts Your Service - October 30, 2018
- XPath and IMT: Namespace Prefixes - October 29, 2018
- Modular RESTlets - October 26, 2018