Implementing a TeamCity Plugin

Total: 15 Average: 3.7

Steps for implementing a TeamCity Plugin include:
• Preparing the development environment
• Generating a Maven project
• Implementing the plugin’s UI
• Organizing the business logic of the plugin.

A plugin in TeamCity is a zip file containing a Java class file, JSP files, resources, and plugin descriptor files.

The basic structure of the plugin

• Build module (contains only configuration files and defines how the plugin is compiled)

◦ plugin-assembly.xml

◦ plugin-agent-assembly.xml

◦ pom.xml

• Agent module (encapsulates logic that will be run on the agent side)

◦ java code

◦ resources

◦ pom.xml

• Common module (a module with shared code)

◦ java code

◦ resources

◦ pom.xml

• Server module (encapsulates logic that will be run on the server side)

◦ java code

◦ resources

◦ pom.xml

• pom.xml (plugin’s main POM file).

Preparing the development environment

Before you start developing a plugin for TeamCity, you must first prepare your environment. Please, make sure that you have:
• JDK 8 installed and JAVA_HOME variable set (the variable contains a path to the JDK software)
• Apache Maven installed
• An IDE that allows developing TeamCity plugins on Java installed (e.g., IntelliJ IDEA).

Generating a Maven project

To develop the Maven plugin for TeamCity, TeamCity Open API is available as a set of Maven artifacts residing in the JetBrains Maven repository. Add the following fragment to the <repositories> section of your pom file to access it:

<repository>
  <id>jetbrains-all</id>
  <url>https://download.jetbrains.com/teamcity-repository</url>
</repository>

To deploy your plugin project faster and more conveniently, you can use one of the three Maven archetypes in the org.jetbrains.teamcity.archetypes group

teamcity-plugin: an empty plugin, includes both the server and the agent plugin parts
teamcity-server-plugin: an empty plugin, includes the server plugin part only
teamcity-sample-plugin: the plugin with the sample code (adds a “Click me” button to the bottom of the TeamCity project Overview page).

The Maven commands to generate projects for different plugins depending on the TeamCity version are given below:

Server-side-only plugin:

mvn org.apache.maven.plugins:maven-archetype-plugin:2.4:generate 
-DarchetypeRepository=https://download.jetbrains.com/teamcity-repository
-DarchetypeArtifactId=teamcity-server-plugin
-DarchetypeGroupId=org.jetbrains.teamcity.archetypes
-DarchetypeVersion=RELEASE
-DteamcityVersion=2018.2

Plugin with both the server and agent parts:

mvn org.apache.maven.plugins:maven-archetype-plugin:2.4:generate
-DarchetypeRepository=https://download.jetbrains.com/teamcity-repository
-DarchetypeArtifactId=teamcity-plugin
-DarchetypeGroupId=org.jetbrains.teamcity.archetypes
-DarchetypeVersion=RELEASE
-DteamcityVersion=2018.2

Sample plugin:

mvn org.apache.maven.plugins:maven-archetype-plugin:2.4:generate
-DarchetypeRepository=https://download.jetbrains.com/teamcity
-repository -DarchetypeArtifactId=teamcity-sample-plugin
-DarchetypeGroupId=org.jetbrains.teamcity.archetypes
-DarchetypeVersion=RELEASE
-DteamcityVersion=2018.2

Regardless of the TeamCity version you’ve selected, you will be asked to enter the Maven groupId, artifactId and the version for your plugin. Please note, that artifactId will be used as your plugin name. After the project is generated, you can update teamcity-plugin.xml in the root directory by entering the display name of the plugin, its description, author email address, and other information.
You can also use the TeamCity SDK Maven plugin, that allows controlling a TeamCity instance from the command line when installing, updating and debugging a new plugin. You can find more information about developing the Maven plugin for TeamCity here.

Implementing the plugin’s UI

The UI for the plugin is typically based on JSP technology. JSP files are usually located in the server-side part of the plugin, just like all other resources necessary for the user interface. A simple example of a JSP file to create a button and process a click is given below.

<%@ include file="/include.jsp" %>
<c:url var="actionUrl" value="/helloUser.html"/>
 
<form action="${actionUrl}"><input class="submitButton" id="search" type="submit" value="Click me!"/></form>

This file can be used to embed JavaScript, refer to Java classes (for example, constants are often used to connect the business logic and the UI), add CSS styles and other resources for the UI. For instance:

<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8" %>

<jsp:useBean id="propertiesBean" scope="request" type="jetbrains.buildServer.controllers.BasePropertiesBean"/>
<jsp:useBean id="cons" class="com.myPackage.StepConstants"/>
 
<c:set var="UserName" value="User Name:"/>
<c:set var="Password" value="Password:"/>
 
<link rel="stylesheet" type="text/css" href="${teamcityPluginResourcesPath}common.css" />
<%@ include file="Styles.jspf" %>
 
<script type="text/javascript">
    (function () {
        // something JS action
    })();
</script>

where:
• the 1st line of code specifies page encoding for the JSP page
• the 2nd line sets the controller class for the JSP page
• the 3rd line registers Java class with constants to connect the business logic and the UI
• the 4th and 5th lines comprise variables declaration
• the 6th and 7th lines link CSS и JSPF files
• the 8th line and rest of the code comprise JS functions declaration and implementation.

This is not a complete list of the JSP pages features. Refer to the tutorials on JSP development to find more information.
There are several approaches to registering and linking JSP pages with Java code, depending on the type of plugin. For example, for Build Runner plugins, it is necessary to implement the inheritance of the RunType class, in the methods of which the JSP pages are indicated (one for the edit mode, one for the view mode).
For example:

public class MyStepRunner extends RunType {
 
    private final PluginDescriptor descriptor;
 
    public MyStepRunner(RunTypeRegistry registry, PluginDescriptor descriptor) {
        this.descriptor = descriptor;
        registry.registerRunType(this);
    }
  
    @NotNull
    @Override
    public String getType() {
        return "StepUniqueName";
    }
 
    @NotNull
    @Override
    public String getDisplayName() {
        return "Step display name";
    }
 
    @NotNull
    @Override
    public String getDescription() {
        return "Step description";
    }
 
    @Nullable
    @Override
    public String getEditRunnerParamsJspFilePath() {
        return descriptor.getPluginResourcesPath("editMyStep.jsp");
    }
 
    @Nullable
    @Override
    public String getViewRunnerParamsJspFilePath() {
        return descriptor.getPluginResourcesPath("viewMyStep.jsp");
    }
 
    @Nullable
    @Override
    public PropertiesProcessor getRunnerPropertiesProcessor() {
        return properties -> {
            // something logic
        };
    }
 
    @Nullable
    @Override
    public Map<String, String> getDefaultRunnerProperties() {
        Map<String, String> map = new HashMap<String, String>();
        return map;
    }
 
    @Nullable
    public Map<String, String> transformParameters(@NotNull Map<String, String> params) {
        return null;
    }
}

Let’s consider the basic moments of this class implementation:
PluginDescriptor is a Maven entity used to describe the process of plugin compilation. We’ll use it to register JSP pages.
RunTypeRegistry comprises an entity to register a plugin step.
getType(), getDisplayName(), getDescription() methods are used to describe basic parameters of the plugin step being implemented.
getEditRunnerParamsJspFilePath() and getViewRunnerParamsJspFilePath() methods are overridden to register JSP pages.
getRunnerPropertiesProcessor(), getDefaultRunnerProperties() and transformParameters methods serve to manipulate step parameters.
This class has many overriding methods that can be useful when developing a plugin. You can find more information about the RunType class here.

Organizing the business logic of the plugin

The plugin’s business logic can be implemented both on the server side and on the agent side (most often). The TeamCity Open API has a set of entities the implementation of which is necessary when developing the business logic of any TeamCity plugin. Let’s consider the example of a Build Runner plugin earnings. Its business logic comes down to implementing the factory interface – CommandLineBuildServiceFactory and extending the BuildServiceAdapter class.

CommandLineBuildServiceFactory
This interface has two methods: createService () and getBuildRunnerInfo (). The first method is used to register a child from the BuildServiceAdapter, and the second one –  to create a AgentBuildRunnerInfo class object. You can find more detailed information here.

An example of this factory implementation:

public class BuildStepFactory implements CommandLineBuildServiceFactory {
 
    @NotNull
    @Override
    public CommandLineBuildService createService() {
        return new MyStepService();
    }
 
    @NotNull
    @Override
    public AgentBuildRunnerInfo getBuildRunnerInfo() {
        return new AgentBuildRunnerInfo() {
 
            @NotNull
            @Override
            public String getType() {
                return "StepUniqueName";
            }
 
            @Override
       public boolean canRun(@NotNull BuildAgentConfiguration agentConfiguration) {
                return true;
            }
        };
    }
}

BuildServiceAdapter

BuildServiceAdapter comprises the main entity for implementing the business logic of the plugin step. This class has many methods and these methods overriding allows you to add preliminary logic, validate parameters before executing a step, execute directly the main logic of the step and post build logic. More information can be found here.
Consider an example of implementing the child of this class:

public class MyStepService extends BuildServiceAdapter {
 
    @NotNull
    @Override
    public ProgramCommandLine makeProgramCommandLine() throws RunBuildException {
 
        AgentRunningBuild build = getRunnerContext().getBuild();
        // something logic with build instance
 
        BuildProgressLogger logger = build.getBuildLogger();
        // something logic with logger instance (output information)
 
        File workingDirectory = getWorkingDirectory(); // get working directory
        Map<String, String> runnerParameters = getRunnerParameters(); // get runner parameters
 
        // something logic for step
 
        SimpleProgramCommandLine simpleProgramCommandLine = new SimpleProgramCommandLine(getRunnerContext(), script, Collections.<String>emptyList());
        return simpleProgramCommandLine;
    }
 
    @Override
    public void beforeProcessStarted() throws RunBuildException {
 
        super.beforeProcessStarted();
        // something logic
    }
 
    @Override
    public void afterProcessFinished() throws RunBuildException {
 
        super.afterProcessFinished();
        // something logic
    }
}

 

Artem Kravets
Latest posts by Artem Kravets (see all)

Artem Kravets

Artem Kravets is a software development team leader at Devart with a bachelor's degree in Computer Engineering and extensive .NET knowledge. He is a ICAgile Certified Professional (ICP) with good experience in .NET desktop development, add-in integration, development of Maven plugins in Java and implementation of CI/CD processes.