Vova Bilyachat

Melbourne, Australia

How to generate and manage release notes with Azure DevOps pipelines and Azure functions for free.

24 November 2018

Problem

I am working on Flymark.dance which is used by many people and its hosted on their local IIS where I am not able to push new releases. For each new release I have to send email to everyone with notes what was changed and where to get the latest version. It works, but not always, because my team members forgot to check their emails and one day most of them instead of using latest version I send them, they used the old version and it caused me a lot of problems. Then I decided to search what solutions are out there to solve my problems and I found quite a few project, but they are not exactly I was looking for.

I wanted my solution to be able to:

  • Check if version is: latest, unstable, outdated
  • Release notes
  • Solution should be fully automated so I don’t need to write any changelog myself

Solution

Azure DevOps Services + Azure Function + Table Storage. Why? Because Azure DevOps Services is complete solution for manage backlog, builds, releases, source control. Its free (you have free private git repositories, free 240 minutes of builds, free Azure function calls).

Managing backlog and linking to release

Board example

Managing backlog is easy with board, and since it’s my pet project we dont have time for much planing so we are using Kanban board and update user stories, bugs as soon as we go.

Its very important to write understandable titles for work items because they will go to change log. To link them with release there are two options:

  • Configure branch policies and make Check for linked work items required Work items
  • other option is that when you are committing specify work item id in commit #task_id then it will be linked automatically

In our project we are using both options :)

Now each release will have history of work items included in this release:

Release work items

Building release name

Depends on a project it can be easy as configure release name or a bit more specific as mine and can be set from script].

For changing Azure DevOps Release name I am using following powershell script

Write-Output "##vso[release.updatereleasename]1.10.12"

Service hooks

We don’t have any api yet but service hooks is where I started. They allow to use webhooks to send full information about release including artifacts, work items. So I create my service hook to dummy non existing API and triggered release, then extracted from logs my request details.

Service hook history

REST Api using Azure Function

So far solution seems to be simple, Create work items in Azure Devops Backlog, Link them to task, Do release, using Service hook will use webhook to send details of release to newly created api.

Http Functions

As a result I’ve build 3 endpoints:

  • release-change-log: [GET] http://localhost:7071/api/release-change-log/{program} - open endpoint
  • release-new-version: [POST] http://localhost:7071/api/release-new-version/{program} - this endpoint is protected with Function authorization
  • release-validate-version: [GET] http://localhost:7071/api/release-validate-version/{program}/{version} - open endpoint

Parameters:

  • program - is any name of an application in my case I am going to use server and mobile.
  • version - 1.0.0, 0.7.1212.2123

Table entity

To save each release I am using azure table storage which is saved using:

    public class FlymarkRelease : TableEntity
    {

        public List<ReleaseNoteItem> ReleaseNotes { get; set; }
        public bool Latest { get; set; }
        public bool Unstable { get; set; }
        public bool ShowInLog { get; set; }
        public string VersionStamp { get; set; }

        public override IDictionary<string, EntityProperty> WriteEntity(OperationContext operationContext)
        {
            var items = base.WriteEntity(operationContext);
            items[nameof(ReleaseNotes)] = new EntityProperty(JsonConvert.SerializeObject(ReleaseNotes));
            return items;
        }

        public override void ReadEntity(IDictionary<string, EntityProperty> properties,
            OperationContext operationContext)
        {
            base.ReadEntity(properties, operationContext);
            if (properties.ContainsKey(nameof(ReleaseNotes)))
            {
                ReleaseNotes =
                    JsonConvert.DeserializeObject<List<ReleaseNoteItem>>(properties[nameof(ReleaseNotes)].StringValue);
            }
        }
    }

There is one important thing to note when designing table storage its very important to define proper RowKey. Since table storage does not support OrderBy, Skip data will be sorted by RowKey. I need to invert my version number and store it as long so I wrote this method to do it.

    public static string ConvertVersionToRowKey(string version)
        {
            int.TryParse(version.Replace(".", ""), out var numbericVersion);
            return (int.MaxValue - numbericVersion).ToString();
        }

All together validation of user version will look like

[FunctionName("release-validate-version")]
public static async Task<HttpResponseMessage> RunValidateVersion(
[HttpTrigger(AuthorizationLevel.Anonymous, "get", Route = "release-validate-version/{program}/{version}")]
HttpRequestMessage req,
string program,
string version,
[Table(TableName)] CloudTable table,
[Blob(Container, FileAccess.Read, Connection = StorageConnectionString)]
CloudBlobContainer container)
{
    var stable = await LatestStableAsync(table, program);
    if (stable.VersionStamp == version)
    {
        return req.CreateResponse(HttpStatusCode.OK, new FlymarkVersionCheck
        {
            IsLatest = true
        });
    }

    var userVersion = await UserVersionAsync(table, program, version);

    var urlToDownload = UrlToDownload(program, container, version);
    return req.CreateResponse(HttpStatusCode.OK, new FlymarkVersionCheck
    {
        IsLatest = false,
        IsUnstable = userVersion?.Unstable ?? false,
        LatestVersion = stable.RowKey,
        Download = urlToDownload
    });
}

 private static async Task<FlymarkRelease> LatestStableAsync(CloudTable table, string key)
{
    var query = new TableQuery<FlymarkRelease>()
        .Where(TableQuery.CombineFilters(
            TableQuery.GenerateFilterCondition(nameof(FlymarkRelease.PartitionKey), QueryComparisons.Equal, key.ToLower()),
            TableOperators.And,
            TableQuery.GenerateFilterConditionForBool(nameof(FlymarkRelease.Latest), QueryComparisons.Equal, true)));

    var result = await table.ExecuteQuerySegmentedAsync(query, null);
    return result.First();
}

private static async Task<FlymarkRelease> UserVersionAsync(CloudTable table, string key, string version)
{
    var rowKey = ConvertVersionToRowKey(version);
    var query = new TableQuery<FlymarkRelease>()
        .Where(TableQuery.CombineFilters(
            TableQuery.GenerateFilterCondition(nameof(FlymarkRelease.PartitionKey), QueryComparisons.Equal, key.ToLower()),
            TableOperators.And,
            TableQuery.GenerateFilterCondition(nameof(FlymarkRelease.RowKey), QueryComparisons.Equal, rowKey)));

    var result = await table.ExecuteQuerySegmentedAsync(query, null);
    return result.FirstOrDefault();
}

As you can see users version is now read by RowKey and getting release notes are now automatically sorted in right order.

Result

Now using previously build endpoints I can compare users version and return details and notify if user needs to update their version

Version need to be update

or just let them know that they are running unstable/beta version

Version is up to date

or all good and its latest version

Version is up to date

And automatic release notes:

Service hook history

In addition, this solution supports multiple release notes simply provide different software name as parameter program and there will be new release notes.

Whats next

Its not yet finished, but there is going to be an api which will change unstable version to be latest stable. In my software there is only one latest version supported, yes there can be multiple unstable but always one stable.

Also for my mobile app I am thinking to return one more property how many versions users are behind the latest version.

Source code

A full source code can be found on github