BuildMaster Documentation

Writing a simple operation

You can easily add new operations to BuildMaster using any .NET language. Below will provide a basic example for creating a simplified version of the Create File operation that's already built-in.

Creating the Project

First, create a new class library project in Visual Studio, and target .NET 4.5. Use NuGet to add a reference to the Inedo.BuildMaster.SDK package.

Make sure that the SDK assemblies added by the NuGet package have the Copy Local property set to False. SDK assemblies should not be included in the extension.

Creating the Operation

Next, create a new public class called CreateFileOperation, and have it inherit the ExecuteOperation class:

public class CreateFileOperation : ExecuteOperation
{
}

Since this operation is supposed to create a file, it would be nice if we could access the file name and what to put in the file as inputs. To do this, we just need to add a couple of properties and attributes:

[Required]
[ScriptAlias("Name")]
[DisplayName("File name")]
public string FileName { get; set; }

[ScriptAlias("Text")]
[DisplayName("Contents")]
[Description("The contents of the file. If this value is missing or empty, a 0-byte file will be created.")]
public string FileText { get; set; }

There's quite a lot of information packed into those two properties, so here's what everything means:

  • [Required] - the user must supply a value for this property for the plan to validate
  • [ScriptAlias(name)] - this is what the name of the property will look like to OtterScript; this value is required and must be unique to the type of operation
  • [DisplayName(name)] - this is displayed in the graphical plan editor to provide a friendlier name than the script alias; this is optional
  • [Description(text)] - another optional attribute that provides some additional help text in the graphical editor

Now that we have our inputs configured, we can write the code that will actually create the file:

public override async Task ExecuteAsync(IOperationExecutionContext context)
{
  	var path = context.ResolvePath(this.FileName);
	var fileOps = context.Agent.GetService<IFileOperationsExecuter>();
	this.LogDebug($"Creating {path}...");
	await fileOps.CreateDirectoryAsync(PathEx.GetDirectoryName(path));
	await fileOps.WriteAllTextAsync(path, this.FileText ?? "");
	this.LogInformation(this.FileName + " file created.");
}

Although the ExecuteAsync method isn't doing too much, we'll break everything down right here:

  • async
    The async keyword in the method declaration instructs the compiler to allow the await keyword in the method body. BuildMaster operations can execute asynchronously; if you don't know what this means, consider reading this MSDN article. If you would prefer to implement ExecuteAsync synchronously, you can just omit the async keyword and return the static Complete property when the method is complete.
  • var path = context.ResolvePath(this.FileName);
    This allows a relative path to be used for the FileName property. Without it, the operation would only work correctly when a user supplies an absolute path. Note that absolute paths will still work; if the second argument of PathEx.Combine is absolute, the first argument is ignored.
  • var fileOps = context.Agent.GetService<IFileOperationsExecuter>();
    This requests a IFileOperationsExecuter service from the BuildMaster agent in the current context. This interface is an abstraction that allows a common set of file system operations on either hosted or SSH agents.
  • await fileOps.CreateDirectoryAsync(PathEx.GetDirectoryName(path));
    This is a simple way to ensure that the directory exists where we are trying to write the file.
  • await fileOps.WriteAllTextAsync(path, this.FileText ?? "");
    This actually writes the desired text to the file.

For brevity, we've left out the logging messages; but those are pretty self-explanatory. Anything logged will be associated with the current operation.

Now we just need to apply a few attributes to the operation itself to make it discoverable to BuildMaster:

[DisplayName("Create File (Example)")]
[Description("Creates a file on a server.")]
[ScriptAlias("Create-File-Example")]
[ScriptNamespace("HDARS")]
[Tag("files")]
[DefaultProperty(nameof(FileName))]
public class CreateFileOperation : ExecuteOperation
{
}

The meaning of DisplayName, Description, and ScriptAlias is the same as for properties. As for the other attributes:

  • ScriptNamespace - specifies a prefix that is used to qualify the name specified in ScriptAlias. For convenience, this attribute can also be applied to the assembly rather than on each operation individually.
  • Tag - specifies a tag which acts as a kind of category for the operation. This attribute can be applied multiple times and can help with discoverability.
  • DefaultProperty - specifies the name of a property on the operation that receives the default argument value. When in script form, the default argument is the value this is passed to the operation positionally, rather than by name. A default is not necessary, but can help increase the readability of a plan when it is viewed or edited as OtterScript.

Rich Descriptions (Optional)

Optionally, you may also override the GetDescription method which allows the plan editor and live execution page to render decorated text for the operation description. Here is an example:

protected override ExtendedRichDescription GetDescription(IOperationConfiguration config)
{
    return new ExtendedRichDescription(
        new RichDescription(
            "Create ",
            new Hilite(config[nameof(this.FileName)])
        ),
        new RichDescription(
            "starting with ",
            new Hilite(config[nameof(this.FileText)])
        )
    );
}

Note the use of config[nameof(this.FileText)] instead of referencing this.FileText directly, this allows the consumer of the method to supply dummy values when there is no contextual value.

Building and Deploying the Extension

With the extension complete (and compiling), we are now reading to package it as an extension and deploy it to BuildMaster. To do this, create a zip file with the same name as the assembly. For example, if the assembly name is MyExample.dll, call the zip file MyExample.zip.

Next, add the .dll file to the zip file, then rename the zip file so that its extension is .bmx.

Copy the .bmx file into the BuildMaster extensions directory. By default, this will usually be in C:\BuildMaster\Extensions, but you can verify the exact location by going to the Admin->All Settings page in BuildMaster and looking for the ExtensionsPath value.

Restart the BuildMaster services (and application pool if hosting in IIS).

Testing the Extension

Go to any plan editor in BuildMaster and verify that your new operation is displayed along with all of the other operations. This create file operation should work on any server, just like the built-in version.