🍰Bringing Cake to Microsoft Teams📣

Sending notifications to Microsoft Teams from your Cake build scripts

Published on Wednesday, 22 February 2017

Microsoft Teams message from Cake

Even though Teams at Microsoft would appreciate you bringing them that’s not what this post is about, but notifications to the Microsoft Teams collaboration / team chat product.

Cake logo

And Cake? Cake is an open source build system, which lets you in an unintrusive way with a C# DSL both cross platform and environment orchestrate everything around your build process — all using existing skills if you’re a .NET developer. ( read more at cakebuild.net )

So that settled, wouldn’t it be great if you could send notifications from your Cake scripts to a Microsoft Teams channel? Guess what — there is!

The Addin

Cake can be extended through addins, Cake addins can be any .NET assembly available as a NuGet package on any nuget feed, where nuget.org is the most well known package source. However you can by using a couple of attributes (i.e. CakeMethodAliasAttribute and CakeNamespaceImportAttribute) become a more “native” Cake addin, in the sense that they lets you extend the DSL and import namespaces using an #addin preprocessor directive.

I’ve created such an addin which makes it easy for you to communicate with a Microsoft Teams channel

#addin nuget:?package=Cake.MicrosoftTeams

MicrosoftTeamsPostMessage("Hello from Cake!",
    new MicrosoftTeamsSettings {
        IncomingWebhookUrl = EnvironmentVariable("MicrosoftTeamsWebHook")
    });

The #addindirective fetches the assembly from nuget, references it, finds any Cake extension methods and imports namespaces — making them conveniently globally available for you in your Cake scripts.

Setup / Teardown

Reporting when script started and when it succeeded / failed can be achieved by registering actions on the Setup and Teardown methods on the Cake script host, Setup is executed before Cake tasks are executed and Teardown is always executed post task execution.

var teamsSettings = new MicrosoftTeamsSettings {
        IncomingWebhookUrl = EnvironmentVariable("MicrosoftTeamsWebHook")
    };

Setup(context => {
    context.MicrosoftTeamsPostMessage("Starting build...", teamsSettings);
});

Teardown(context => {
    context.MicrosoftTeamsPostMessage(context.Successful ? "Build completed successfully."
        : string.Format("Build failed.\r\n({0})", context.ThrownException),
        teamsSettings);
})

As you can see the teardown context has an Successful property indicating build success/failure and ThrownException property containing exception on failure. A successful build would look something like this:

Cake Microsoft Teams Build started & succeeded messages

And a failed build would contain the exception / stack trace like this:

Cake Microsoft Teams Build started and failed messages

Task Setup / Teardown

If you want to track the progress of individual tasks that’s possible using the TaskSetup and TaskTeardown, which lets you register actions executed before and after task is executed.

TaskSetup(context => {
    context.MicrosoftTeamsPostMessage(string.Format("Starting task {0}...", context.Task.Name),
        teamsSettings);
});

TaskTeardown(context => {
    context.MicrosoftTeamsPostMessage(string.Format("Task {0} {1} ({2}).",
        context.Task.Name,
        context.Skipped ? "was skipped" : "processed",
        context.Duration),
        teamsSettings);
});

Both setup and teardown context provides a Task property which gives you meta data about the task, the teardown context also provides Duration and Skipped properties indicating if task was executed and how long it took to execute the task.

Advanced formatting

So besides add in method MicrosoftTeamsPostMessage that just takes a string as message, there’s also a MicrosoftTeamsPostMessage overload that takes an MicrosoftTeamsMessageCard, this gives you more control of the message layout.

Posting a message for each step of the build can become very chatty, tailoring the message to instead neatly summarize the build in one message like below is probably more sustainable in the long run.

Advanced formatted Teams Message from Cake

Which is very similar to what Cake outputs to the console

Cake task summary

On failure you’ll get the icon clearly indicating failure and steps executed just as on success.

Failed task Microsoft Teams Message from Cake

But you also get the full stacktrace from any error when expanding the message

Full failed task Microsoft Teams message from Cake

So what could the code for this look like? A complete example you can test below

#addin nuget:?package=Cake.MicrosoftTeams
string projectName = "Example";
string microsoftTeamsWebHook = EnvironmentVariable("MicrosoftTeamsWebHook");
DateTime startDate = DateTime.UtcNow;

if (string.IsNullOrEmpty(microsoftTeamsWebHook))
{
    throw new Exception("MicrosoftTeamsWebHook environment variable not specified.");
}

var teamsSettings = new MicrosoftTeamsSettings { IncomingWebhookUrl = microsoftTeamsWebHook };
var facts = new List<MicrosoftTeamsMessageFacts>();

Setup(context => {
    facts.Add(new MicrosoftTeamsMessageFacts {
        name = "Setup",
        value = startDate.ToString("yyyy-MM-dd HH:mm:ss")
    });
});

TaskTeardown(context => {
    facts.Add(new MicrosoftTeamsMessageFacts {
        name = string.Concat(context.Task.Name),
        value = context.Skipped ? "skipped" : context.Duration.ToString("c")
    });
});

Teardown(context => {
    var tearDownDate = DateTime.UtcNow;
    facts.Insert(0,
        new MicrosoftTeamsMessageFacts {
            name = "Success",
            value = context.Successful ? "Yes" : "No"
        });

    facts.Add(new MicrosoftTeamsMessageFacts {
        name = "Teardown",
        value = tearDownDate.ToString("yyyy-MM-dd HH:mm:ss")
    });

    facts.Add(new MicrosoftTeamsMessageFacts {
        name = "Total Duration",
        value = (tearDownDate - startDate).ToString()
    });

    if (context.ThrownException!=null)
    {
        facts.Add(new MicrosoftTeamsMessageFacts {
            name = "Exception",
            value = context.ThrownException.ToString()
        });
    }

    var activityImage = context.Successful  ? "https://cloud.githubusercontent.com/assets/1647294/21986014/cf83fdb0-dbfd-11e6-8d18-617b0fd17597.png"
                                            : "https://cloud.githubusercontent.com/assets/1647294/21986029/ec3256a0-dbfd-11e6-9300-f183681cee85.png";

    var messageCard = new MicrosoftTeamsMessageCard {
                          summary = string.Format("{0} build {1}.", projectName, context.Successful ? "succeeded" : "failed"),
                          title = string.Format("Build {0}", projectName),
                          sections = new []{
                              new MicrosoftTeamsMessageSection{
                                  activityTitle = string.Format("{0} build {1}.", projectName, context.Successful ? "succeeded" : "failed"),
                                  activitySubtitle = string.Format("using Cake version {0} on {1}",
                                    Context.Environment.Runtime.CakeVersion,
                                    Context.Environment.Runtime.TargetFramework),
                                  activityText = "Build details",
                                  activityImage = activityImage,
                                  facts = facts.ToArray()
                              }
                          }
                        };

    Information("{0}", context.MicrosoftTeamsPostMessage(messageCard, teamsSettings));
});


Task("Restore")
    //Imagine actual work being done
    .Does(()=>System.Threading.Thread.Sleep(8000));

Task("Build")
    .IsDependentOn("Restore")
    //Imagine actual work being done
    .Does(()=>System.Threading.Thread.Sleep(6000));

Task("Test")
    .IsDependentOn("Build")
    //Imagine actual work being done
    .Does(()=>System.Threading.Thread.Sleep(15000));

Task("Package")
    .IsDependentOn("Test")
    //Imagine actual work being done
    .Does(()=>System.Threading.Thread.Sleep(5000));

Task("Publish")
    .IsDependentOn("Package")
    //Imagine actual work being done
    .Does(()=>System.Threading.Thread.Sleep(11000));

RunTarget("Publish");

The above code basically hooks up actions to Setup, Teardown and TaskTeardown. Only collecting data before Teardown is finally called. It’s just C# so you can go as crazy as you want, but above is a good starting point.

Conclusion

Hopefully this post has shown you how to send notifications from your cake scripts and what the extension points Cake provides for this. This could obviously be reused for other messaging platforms and if you check the addins section on Cake’s website you’ll find addins for Slack, HipChat, Gitter, Twitter, etc.