Build Metadata Provider¶
This section covers the details of customizing Reqnroll’s build metadata functionality for custom source control management (SCM) and build systems.
Overview¶
Reqnroll provides built-in support for extracting build metadata from various CI/CD environments including Azure Pipelines, TeamCity, Jenkins, GitHub Actions, GitLab CI, and many others. This metadata is captured through the IBuildMetadataProvider interface and stored in a BuildMetadata record.
When working with custom or unsupported build systems, you can create a runtime plugin that provides your own implementation of IBuildMetadataProvider to extract the relevant build information for your specific environment.
BuildMetadata Record Properties {#build-metadata-record}¶
The BuildMetadata record contains the following properties that capture essential build and source control information:
Core Properties¶
Property |
Type |
Description |
|---|---|---|
|
|
The URL to the build in the CI/CD system (e.g., link to build results page) |
|
|
The unique identifier or number assigned to the build by the CI/CD system |
|
|
The URL of the source control repository (e.g., Git remote URL) |
|
|
The specific commit hash, revision, or changeset identifier |
|
|
The source control branch name from which the build was triggered |
|
|
The source control tag name if the build was triggered from a tagged commit |
|
|
The name of the build server or CI/CD product (set automatically by the provider) |
Property Details¶
BuildUrl: This should link directly to the build results page where developers can view build logs, artifacts, and status. Format varies by CI/CD system.
BuildNumber: Often an incrementing integer but can be any string format depending on your build system’s numbering scheme.
Remote: Typically a Git repository URL, but can be any source control system URL. Should be in a format that allows cloning or accessing the repository.
Revision: For Git this would be the full commit SHA. For other SCM systems, use the equivalent unique identifier.
Branch: The branch name without any prefix (e.g., “main”, “develop”, “feature/new-functionality”).
Tag: Only set if the build was triggered from a tagged commit. Should be the tag name without any prefix.
ProductName: Automatically set by the GetBuildMetadata() method to identify which build system provided the metadata.
Creating a Custom Build Metadata Provider {#custom-build-metadata-provider}¶
To support a custom SCM or build system, you need to create a Reqnroll runtime plugin that implements the IBuildMetadataProvider interface.
Step 1: Create the Plugin Project¶
Create a new .NET class library project that will contain your custom build metadata provider:
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>netstandard2.0</TargetFramework>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Reqnroll" Version="2.4.0" />
</ItemGroup>
</Project>
Note: Target .NET Standard 2.0 for maximum compatibility with all .NET versions supported by Reqnroll.
Step 2: Implement IBuildMetadataProvider¶
Create your custom implementation of the IBuildMetadataProvider interface:
using Reqnroll.EnvironmentAccess;
namespace MyCompany.CustomScm.ReqnrollPlugin
{
public class CustomBuildMetadataProvider : IBuildMetadataProvider
{
private readonly IEnvironmentWrapper _environment;
public CustomBuildMetadataProvider(IEnvironmentWrapper environment)
{
_environment = environment;
}
public BuildMetadata GetBuildMetadata()
{
// Check if we're running in your custom build environment
var customBuildId = GetEnvironmentVariable("CUSTOM_BUILD_ID");
if (string.IsNullOrEmpty(customBuildId))
return null; // Not running in our custom environment
// Extract metadata from your custom environment variables
var buildUrl = GetEnvironmentVariable("CUSTOM_BUILD_URL");
var buildNumber = GetEnvironmentVariable("CUSTOM_BUILD_NUMBER");
var remote = GetEnvironmentVariable("CUSTOM_SCM_REMOTE");
var revision = GetEnvironmentVariable("CUSTOM_SCM_REVISION");
var branch = GetEnvironmentVariable("CUSTOM_SCM_BRANCH");
var tag = GetEnvironmentVariable("CUSTOM_SCM_TAG");
return new BuildMetadata(buildUrl, buildNumber, remote, revision, branch, tag)
{
ProductName = "Custom Build System"
};
}
private string GetEnvironmentVariable(string variable)
{
var result = _environment.GetEnvironmentVariable(variable);
return result is ISuccess<string> success ? success.Result : null;
}
}
}
Step 3: Create the Runtime Plugin¶
Create a class that implements IRuntimePlugin to register your custom provider:
using Reqnroll.BoDi;
using Reqnroll.Plugins;
using Reqnroll.EnvironmentAccess;
[assembly: RuntimePlugin(typeof(MyCompany.CustomScm.ReqnrollPlugin.CustomBuildMetadataPlugin))]
namespace MyCompany.CustomScm.ReqnrollPlugin
{
public class CustomBuildMetadataPlugin : IRuntimePlugin
{
public void Initialize(RuntimePluginEvents runtimePluginEvents,
RuntimePluginParameters runtimePluginParameters,
UnitTestProviderConfiguration unitTestProviderConfiguration)
{
// Register our custom implementation in the global container
runtimePluginEvents.CustomizeGlobalDependencies += (sender, args) =>
{
args.ObjectContainer.RegisterTypeAs<CustomBuildMetadataProvider, IBuildMetadataProvider>();
};
}
}
}
Step 4: Build and Deploy the Plugin¶
Build your plugin project to generate the assembly
Name the output assembly with the suffix
.ReqnrollPlugin.dll(e.g.,MyCompany.CustomScm.ReqnrollPlugin.dll)Deploy the plugin by copying it to one of these locations:
The folder containing your
Reqnroll.dllfileYour working directory
Reqnroll automatically discovers and loads plugins with the .ReqnrollPlugin.dll naming convention.
Step 5: Configure Environment Variables¶
Ensure your custom build system sets the appropriate environment variables that your provider expects:
# Example environment variables for your custom system
export CUSTOM_BUILD_ID="12345"
export CUSTOM_BUILD_URL="https://build.mycompany.com/builds/12345"
export CUSTOM_BUILD_NUMBER="1.2.3-build.12345"
export CUSTOM_SCM_REMOTE="https://scm.mycompany.com/repo/myproject.git"
export CUSTOM_SCM_REVISION="a1b2c3d4e5f6789012345678901234567890abcd"
export CUSTOM_SCM_BRANCH="main"
export CUSTOM_SCM_TAG="" # Empty if not a tag build
Advanced Scenarios¶
Supporting Multiple Build Systems¶
If you need to support multiple custom build systems, you can create a composite provider:
public class CompositeBuildMetadataProvider : IBuildMetadataProvider
{
private readonly IEnvironmentWrapper _environment;
private readonly IBuildMetadataProvider[] _providers;
public CompositeBuildMetadataProvider(IEnvironmentWrapper environment)
{
_environment = environment;
_providers = new IBuildMetadataProvider[]
{
new CustomSystemAProvider(environment),
new CustomSystemBProvider(environment),
new LegacySystemProvider(environment)
};
}
public BuildMetadata GetBuildMetadata()
{
foreach (var provider in _providers)
{
var metadata = provider.GetBuildMetadata();
if (metadata != null)
return metadata;
}
return null; // No custom system detected
}
}
Fallback to Default Provider¶
To ensure compatibility with standard CI/CD systems while adding support for your custom system:
public class CustomWithFallbackBuildMetadataProvider : IBuildMetadataProvider
{
private readonly IBuildMetadataProvider _customProvider;
private readonly IBuildMetadataProvider _defaultProvider;
public CustomWithFallbackBuildMetadataProvider(
IEnvironmentWrapper environment,
IEnvironmentInfoProvider environmentInfoProvider)
{
_customProvider = new CustomBuildMetadataProvider(environment);
_defaultProvider = new BuildMetadataProvider(environmentInfoProvider, environment);
}
public BuildMetadata GetBuildMetadata()
{
// Try custom provider first
var customMetadata = _customProvider.GetBuildMetadata();
if (customMetadata != null)
return customMetadata;
// Fallback to default provider for standard CI/CD systems
return _defaultProvider.GetBuildMetadata();
}
}
Handling Complex Environment Detection¶
For sophisticated environment detection logic:
public class SmartBuildMetadataProvider : IBuildMetadataProvider
{
private readonly IEnvironmentWrapper _environment;
public SmartBuildMetadataProvider(IEnvironmentWrapper environment)
{
_environment = environment;
}
public BuildMetadata GetBuildMetadata()
{
// Detect environment based on multiple indicators
if (IsCustomSystemA())
return GetCustomSystemAMetadata();
if (IsCustomSystemB())
return GetCustomSystemBMetadata();
if (IsDockerEnvironment())
return GetDockerEnvironmentMetadata();
return null;
}
private bool IsCustomSystemA()
{
return !string.IsNullOrEmpty(GetVariable("CUSTOM_A_BUILD_ID")) &&
!string.IsNullOrEmpty(GetVariable("CUSTOM_A_PROJECT"));
}
private bool IsCustomSystemB()
{
var buildTool = GetVariable("BUILD_TOOL");
var buildId = GetVariable("BUILD_IDENTIFIER");
return "CustomTool".Equals(buildTool, StringComparison.OrdinalIgnoreCase) &&
!string.IsNullOrEmpty(buildId);
}
private bool IsDockerEnvironment()
{
return File.Exists("/.dockerenv") ||
!string.IsNullOrEmpty(GetVariable("DOCKER_CONTAINER_ID"));
}
private string GetVariable(string name)
{
var result = _environment.GetEnvironmentVariable(name);
return result is ISuccess<string> success ? success.Result : null;
}
// Implementation methods for GetCustomSystemAMetadata(), etc.
}
Testing Your Custom Provider¶
Create unit tests to verify your custom provider works correctly:
[Test]
public void GetBuildMetadata_WithCustomEnvironmentVariables_ReturnsBuildMetadata()
{
// Arrange
var mockEnvironment = new Mock<IEnvironmentWrapper>();
mockEnvironment.Setup(e => e.GetEnvironmentVariable("CUSTOM_BUILD_ID"))
.Returns(new Success<string>("12345"));
mockEnvironment.Setup(e => e.GetEnvironmentVariable("CUSTOM_BUILD_URL"))
.Returns(new Success<string>("https://build.example.com/12345"));
// ... setup other variables
var provider = new CustomBuildMetadataProvider(mockEnvironment.Object);
// Act
var result = provider.GetBuildMetadata();
// Assert
Assert.That(result, Is.Not.Null);
Assert.That(result.BuildNumber, Is.EqualTo("12345"));
Assert.That(result.BuildUrl, Is.EqualTo("https://build.example.com/12345"));
Assert.That(result.ProductName, Is.EqualTo("Custom Build System"));
}
[Test]
public void GetBuildMetadata_WithoutCustomEnvironment_ReturnsNull()
{
// Arrange
var mockEnvironment = new Mock<IEnvironmentWrapper>();
mockEnvironment.Setup(e => e.GetEnvironmentVariable(It.IsAny<string>()))
.Returns(new Success<string>(null));
var provider = new CustomBuildMetadataProvider(mockEnvironment.Object);
// Act
var result = provider.GetBuildMetadata();
// Assert
Assert.That(result, Is.Null);
}
Container Registration Details¶
The plugin system uses Reqnroll’s built-in BoDi dependency injection container. The key points for container registration:
Registration Methods¶
RegisterTypeAs<TImplementation, TInterface>(): Registers a type to be instantiated when the interface is requestedRegisterInstanceAs<TInterface>(instance): Registers a pre-created instanceRegisterFactoryAs<TInterface>(factory): Registers a factory function
Container Hierarchy¶
Reqnroll uses a hierarchical container system:
Global Container: For global services (where you register
IBuildMetadataProvider)Test Thread Container: Per test thread
Feature Container: Per feature execution
Scenario Container: Per scenario execution
Registration Events¶
RegisterGlobalDependencies: For new interface registrationsCustomizeGlobalDependencies: For overriding existing registrations (recommended forIBuildMetadataProvider)
The CustomizeGlobalDependencies event is used because IBuildMetadataProvider is already registered by Reqnroll’s default implementation, and you want to override it with your custom implementation.
Best Practices¶
Environment Detection: Always check if you’re running in your target environment before returning metadata
Error Handling: Return
nullwhen your environment is not detected rather than throwing exceptionsFallback Support: Consider supporting fallback to the default provider for standard CI/CD systems
Testing: Write comprehensive unit tests for your provider logic
Documentation: Document the environment variables your provider expects
Versioning: Use semantic versioning for your plugin to manage compatibility
Performance: Cache expensive operations if environment variable access is costly in your system
By following this guide, you can successfully extend Reqnroll to work with any custom SCM or build system while maintaining compatibility with existing functionality.