Click here to Skip to main content
13,197,548 members (51,450 online)
Click here to Skip to main content
Add your own
alternative version

Tagged as

Stats

10.7K views
8 bookmarked
Posted 30 Apr 2016

Jenkins C# API Library for Triggering Builds

, 30 Apr 2016
Rate this:
Please Sign up or sign in to vote.
Find out how to use the Jenkins API in C# code. C# library for triggering parameterized builds and ability to wait until the builds' finish.

In my series dedicated to the Jenkins automation service, you can find lots of information how to automate your CI tests' execution.  Usually, to use the most of the functionalities of Jenkins you don't need any coding efforts. However, sometimes there are cases where you want to integrate it with other tools like TFS build or something similar. Jenkins provides excellent REST API. In this article, I am going to show you how to create a Jenkins C# library that is based on the Jenkins REST API. The Jenkins C# API is going to provide methods for triggering and controlling the builds' execution.

Jenkins C# Library Use Case

My team needed a way to trigger Jenkins builds and controlling their execution via C# code. However, there isn't any official C# library, so we decided to write our own. I read about the Jenkins built-in REST API and decided to write a C# wrapper based on it. I had to write something that triggers parameterized builds, waits for them to finish and returns the builds' status- success or failure. We needed to integrate our system tests' execution with our custom deployment tool. Our system tests are run through Jenkins builds. The newly generated product's package should be deployed only if the system tests' build is green. Because of that, the deployment workflow should first be able to trigger a new build, wait for it to finish and decide what to do based on the build's output.

First, we get the next build number so that we can check later for the status of the build. When we trigger a new build, we don't know its number. If there isn't a concurrent build run, we are aware that the previous next build number is our current build's number when the build starts. So we wait until the build starts, usually for 30 seconds. Then we wait for the build's finish and return its status.

Jenkins REST API

You can find а detailed information about the Jenkins API in the footer of every Jenkins page.

When you click on the URL, a new page is opened with a detailed information about the Jenkins REST API.

There you can find the exact URLs that you need to use.

Jenkins C# Library Structure

Find below the structure of our Jenkins C# library.

I used the Onion Architecture to make the different components of the library testable. The concept of this architecture was introduced by Jeffery Palermo in 2008 with the aim to make the applications loosely coupled, with proper separation between the folders and the different areas of concerns in the app. It eases the development, the testing, and the maintenance.

<grammarly-btn>

The HttpAdapter class contains methods for creating HTTP requests. It is placed under the Infrastructure folder because it contains an external dependancy- System.Net. Under the Services folder, you can find the core logic of the Jenkins C# library. The BuildAdapter class includes most of the methods that are used to trigger builds, and the whole workflow's logic is wrapped in the BuildService class.

Jenkins C# Library Code

HTTP Module- HttpAdapter

This class contains two methods- Get and Post. The other services use it as an interface so that it can be mocked in the unit tests.

public class HttpAdapter : IHttpAdapter
{
    public string Get(string url)
    {
        string responseString = string.Empty;
        var request = (HttpWebRequest)WebRequest.Create(url);
        var httpResponse = (HttpWebResponse)request.GetResponse();
        Stream resStream = httpResponse.GetResponseStream();
        var reader = new StreamReader(resStream);
        responseString = reader.ReadToEnd();
        resStream.Close();
        reader.Close();

        return responseString;
    }

    public string Post(string url, string postData)
    {
        WebRequest request = WebRequest.Create(url);
        request.Method = "POST";
        byte[] byteArray = Encoding.UTF8.GetBytes(postData);
        request.ContentType = "application/x-www-form-urlencoded";
        request.ContentLength = byteArray.Length;
        Stream dataStream = request.GetRequestStream();
        dataStream.Write(byteArray, 0, byteArray.Length);
        dataStream.Close();
        WebResponse response = request.GetResponse();
        dataStream = response.GetResponseStream();
        var reader = new StreamReader(dataStream);
        string responseFromServer = reader.ReadToEnd();
        reader.Close();
        dataStream.Close();
        response.Close();

        return responseFromServer;
    }
}

BuildAdapter Explained

public string TriggerBuild(string tfsBuildNumber)
{
    string response = this.httpAdapter.Post(
        this.parameterizedQueueBuildUrl, 
        string.Concat("TfsBuildNumber=", tfsBuildNumber));

    return response;
}

This is the method that triggers new parameterized builds based on the URL generated by GenerateParameterizedQueueBuildUrl.

<grammarly-btn>

internal string GenerateParameterizedQueueBuildUrl(
string jenkinsServerUrl, 
string projectName)
{
    string resultUrl = string.Empty;
    Uri result = default(Uri);
    if (Uri.TryCreate(
    string.Concat(jenkinsServerUrl, "/job/", projectName, "/buildWithParameters"), 
    UriKind.Absolute, 
    out result))
    {
        resultUrl = result.AbsoluteUri;
    }
    else
    {
        throw new ArgumentException(
            "The Parameterized Queue Build Url was not created correctly.");
    }

    return resultUrl;
}

The generated URL is similar to this one- http://localhost:8080/job/Jenkins-CSharp-Api.Parameterized/buildWithParameters. The parameters of the build are passed in the body of the HTTP request.

internal string GenerateBuildStatusUrl(string jenkinsServerUrl, string projectName)
{
    string resultUrl = string.Empty;
    Uri result = default(Uri);
    if (Uri.TryCreate(
    string.Concat(jenkinsServerUrl, "/job/", projectName, "/api/xml"),
    UriKind.Absolute,
    out result))
    {
        resultUrl = result.AbsoluteUri;
    }
    else
    {
        throw new ArgumentException(
        "The Build status Url was not created correctly.");
    }

    return resultUrl;
}

The URL for getting the build's data is a little bit different- http://localhost:8080/job/Jenkins-CSharp-Api.Parameterized/api/xml. If you create a GET HTTP request, you will receive the following XML.

<grammarly-btn>

<freeStyleProject>
<action>
<parameterDefinition>
<defaultParameterValue>
<value/>
</defaultParameterValue>
<description/>
<name>TfsBuildNumber</name>
<type>StringParameterDefinition</type>
</parameterDefinition>
</action>
<action/>
<description/>
<displayName>Jenkins-CSharp-Api.Parameterized</displayName>
<name>Jenkins-CSharp-Api.Parameterized</name>
<url>
http://jenkins.aangelov.com/job/Jenkins-CSharp-Api.Parameterized/
</url>
<buildable>true</buildable>
<build>
<number>5</number>
<url>
http://jenkins.aangelov.com/job/Jenkins-CSharp-Api.Parameterized/5/
</url>
</build>
<build>
<number>4</number>
<url>
http://jenkins.aangelov.com/job/Jenkins-CSharp-Api.Parameterized/4/
</url>
</build>
<build>
<number>3</number>
<url>
http://jenkins.aangelov.com/job/Jenkins-CSharp-Api.Parameterized/3/
</url>
</build>
<build>
<number>2</number>
<url>
http://jenkins.aangelov.com/job/Jenkins-CSharp-Api.Parameterized/2/
</url>
</build>
<build>
<number>1</number>
<url>
http://jenkins.aangelov.com/job/Jenkins-CSharp-Api.Parameterized/1/
</url>
</build>
<color>blue</color>
<firstBuild>
<number>1</number>
<url>
http://jenkins.aangelov.com/job/Jenkins-CSharp-Api.Parameterized/1/
</url>
</firstBuild>
<healthReport>
<description>Build stability: No recent builds failed.</description>
<iconClassName>icon-health-80plus</iconClassName>
<iconUrl>health-80plus.png</iconUrl>
<score>100</score>
</healthReport>
<inQueue>false</inQueue>
<keepDependencies>false</keepDependencies>
<lastBuild>
<number>5</number>
<url>
http://jenkins.aangelov.com/job/Jenkins-CSharp-Api.Parameterized/5/
</url>
</lastBuild>
<lastCompletedBuild>
<number>5</number>
<url>
http://jenkins.aangelov.com/job/Jenkins-CSharp-Api.Parameterized/5/
</url>
</lastCompletedBuild>
<lastStableBuild>
<number>5</number>
<url>
http://jenkins.aangelov.com/job/Jenkins-CSharp-Api.Parameterized/5/
</url>
</lastStableBuild>
<lastSuccessfulBuild>
<number>5</number>
<url>
http://jenkins.aangelov.com/job/Jenkins-CSharp-Api.Parameterized/5/
</url>
</lastSuccessfulBuild>
<nextBuildNumber>6</nextBuildNumber>
<property>
<parameterDefinition>
<defaultParameterValue>
<name>TfsBuildNumber</name>
<value/>
</defaultParameterValue>
<description/>
<name>TfsBuildNumber</name>
<type>StringParameterDefinition</type>
</parameterDefinition>
</property>
<concurrentBuild>false</concurrentBuild>
<scm/>
</freeStyleProject>

You can also get a specific build's data through this URL- http://localhost:8080/job/Jenkins-CSharp-Api.Parameterized/5/api/xml. It is generated by the code below.

<grammarly-btn>

internal string GenerateSpecificBuildNumberStatusUrl(
string buildNumber,
string jenkinsServerUrl, 
string projectName)
{
    string generatedUrl = string.Empty;
    Uri result = default(Uri);
    if (Uri.TryCreate(
    string.Concat(jenkinsServerUrl, "/job/", projectName, "/", buildNumber, "/api/xml"),
    UriKind.Absolute,
    out result))
    {
        generatedUrl = result.AbsoluteUri;
    }
    else
    {
        throw new ArgumentException(
        "The Specific Build Number Url was not created correctly.");
    }

    return generatedUrl;
}

We use a logic from the System. <g class="gr_ gr_12 gr-alert gr_spell gr_run_anim ContextualSpelling ins-del multiReplace" data-gr-id="12" id="12">Xml .Linq namespace to extract specific data from the returned XML documents.

<grammarly-btn>

internal string GetXmlNodeValue(string xmlContent, string xmlNodeName)
{
    IEnumerable<XElement> foundElemenets = 
    this.GetAllElementsWithNodeName(xmlContent, xmlNodeName);

    if (foundElemenets.Count() == 0)
    {
        throw new Exception(
            string.Format("No elements were found for node {0}", xmlNodeName));
    }
    string elementValue = foundElemenets.First().Value;

    return elementValue;
}

internal IEnumerable<XElement> GetAllElementsWithNodeName(
    string xmlContent,
    string xmlNodeName)
{
    XDocument document = XDocument.Parse(xmlContent);
    XElement root = document.Root;
    IEnumerable<XElement> foundElemenets = 
    from element in root.Descendants(xmlNodeName)
         select element;

    return foundElemenets;
}

The rest of the public methods of the BuildAdapter class are used to return specific data from the different Jenkins REST API responses. GetQueuedBuildNumber returns the currently triggered build's number. GetBuildTfsBuildNumber gets the TfsBuildNumber parameter of the build.

public int GetQueuedBuildNumber(string xmlContent, string queuedBuildName)
{
    IEnumerable<XElement> buildElements = 
    this.GetAllElementsWithNodeName(xmlContent, "build");
    string nextBuildNumberStr = string.Empty;
    int nextBuildNumber = -1;

    foreach (XElement currentElement in buildElements)
    {
        nextBuildNumberStr = currentElement.Element("number").Value;
        string currentBuildSpecificUrl = 
        this.GenerateSpecificBuildNumberStatusUrl(
        nextBuildNumberStr, 
        this.jenkinsServerUrl, 
        this.projectName);
        string newBuildStatus = this.httpAdapter.Get(currentBuildSpecificUrl);
        string currentBuildName = this.GetBuildTfsBuildNumber(newBuildStatus);
        if (queuedBuildName.Equals(currentBuildName))
        {
            nextBuildNumber = int.Parse(nextBuildNumberStr);
            Debug.WriteLine("The real build number is {0}", nextBuildNumber);
            break;
        }
    }
    if (nextBuildNumber == -1)
    {
        throw new Exception(
        string.Format(
        "Build with name {0} was not find in the queued builds.", 
        queuedBuildName));
    }

    return nextBuildNumber;
}

public string GetBuildTfsBuildNumber(string xmlContent)
{
    IEnumerable<XElement> foundElements = 
    from el in this.GetAllElementsWithNodeName(xmlContent, "parameter").Elements()
            where el.Value == "TfsBuildNumber"
            select el;

    if (foundElements.Count() == 0)
    {
        throw new ArgumentException("The TfsBuildNumber was not set!");
    }
    string tfsBuildNumber = 
    foundElements.First().NodesAfterSelf().OfType<XElement>().First().Value;

    return tfsBuildNumber;
}

public bool IsProjectBuilding(string xmlContent)
{
    bool isBuilding = false;
    string isBuildingStr = this.GetXmlNodeValue(xmlContent, "building");
    isBuilding = bool.Parse(isBuildingStr);

    return isBuilding;
}

public string GetBuildResult(string xmlContent)
{
    string buildResult = this.GetXmlNodeValue(xmlContent, "result");
    return buildResult;
}

public string GetNextBuildNumber(string xmlContent)
{
    string nextBuildNumber = this.GetXmlNodeValue(xmlContent, "nextBuildNumber");
    return nextBuildNumber;
}

public string GetUserName(string xmlContent)
{
    string userName = this.GetXmlNodeValue(xmlContent, "userName");
    return userName;
}

BuildService Explained

The whole build workflow previously explained can be found in the Run method of the BuildService class.

public string Run(string tfsBuildNumber)
{
    if (string.IsNullOrEmpty(tfsBuildNumber))
    {
        tfsBuildNumber = Guid.NewGuid().ToString();
    }
    string nextBuildNumber = this.GetNextBuildNumber();
    this.TriggerBuild(tfsBuildNumber, nextBuildNumber);
    this.WaitUntilBuildStarts(nextBuildNumber);
    string realBuildNumber = this.GetRealBuildNumber(tfsBuildNumber);
    this.buildAdapter.InitializeSpecificBuildUrl(realBuildNumber);
    this.WaitUntilBuildFinish(realBuildNumber);
    string buildResult = this.GetBuildStatus(realBuildNumber);

    return buildResult;
}

One of the most important methods is present in TriggerBuild. If a concurrent build has been triggered with the same build number an exception is thrown.

<grammarly-btn>

internal string TriggerBuild(string tfsBuildNumber, string nextBuildNumber)
{
    string buildStatus = string.Empty;
    bool isAlreadyBuildTriggered = false;
    try
    {
        buildStatus = this.buildAdapter.GetSpecificBuildStatusXml(nextBuildNumber);
        Debug.WriteLine(buildStatus);
    }
    catch (WebException ex)
    {
        if (!ex.Message.Equals("The remote server returned an error: (404) Not Found."))
        {
            isAlreadyBuildTriggered = true;
        }
    }
    if (isAlreadyBuildTriggered)
    {
        throw new Exception("Another build with the same build number is already triggered.");
    }
    string response = this.buildAdapter.TriggerBuild(tfsBuildNumber);

    return response;
}

Another equally important method is the one for waiting the build to finish.

internal void WaitUntilBuildFinish(string realBuildNumber)
{
    bool shouldContinue = false;
    string buildStatus = string.Empty;
    do
    {
        buildStatus = this.buildAdapter.GetSpecificBuildStatusXml(realBuildNumber);
        bool isProjectBuilding = this.buildAdapter.IsProjectBuilding(buildStatus);
        if (!isProjectBuilding)
        {
            shouldContinue = true;
        }
        Debug.WriteLine("Waits 5 seconds before the new check if the build is completed...");
        Thread.Sleep(5000);
    }
    while (!shouldContinue);
}

If the build is still in an execution mode, we wait for 5 seconds and check again.

Summary

Jenkins supports building, deploying and automating any project. Its REST API gives limitless possibilities for integration with other applications- commercial or home-made. You can wrap the REST API in any language and create custom workflows. You can download the full source of my custom solution plus all of its unit and integration tests.

 

The post Jenkins C# API Library for Triggering Builds appeared first on Automate The Planet

All images are purchased from DepositPhotos.com and cannot be downloaded and used for free.
License Agreement

License

This article, along with any associated source code and files, is licensed under The Microsoft Public License (Ms-PL)

Share

About the Author

Anton Angelov
CEO Automate The Planet
Bulgaria Bulgaria
Anton Angelov is an IT Consultant and Quality Assurance Architect at Innovative Lab. He is passionate about automation testing and designing test harness and tools, having the best industry development practices in mind. In addition, he is an active blogger and the founder of Automate The Planet. He strives to make the site one of the leading authorities in Automation Testing by presenting compelling articles, inspiring ardent discussions amongst the community. He is also one of the most-rated-answer authors of questions about Test Automation Frameworks (WebDriver) on Stack Overflow.

You may also be interested in...

Pro
Pro

Comments and Discussions

 
-- There are no messages in this forum --
Permalink | Advertise | Privacy | Terms of Use | Mobile
Web04 | 2.8.171020.1 | Last Updated 30 Apr 2016
Article Copyright 2016 by Anton Angelov
Everything else Copyright © CodeProject, 1999-2017
Layout: fixed | fluid