Skip to content

Conversation

evgenyfedorov2
Copy link
Member

@evgenyfedorov2 evgenyfedorov2 commented Jul 17, 2025

Adding a brand new component - Build metadata, which automatically grabs build information from the CI/CD pipelines, deserializes it into a strong type BuildMetadata and registers it in Dependency Injection container as IOptions<BuildMetadata>.
The component uses source generation to collect build information and immediately express it via C# code.

Initially, only GitHub Actions and Azure DevOps are supported

Microsoft Reviewers: Open in CodeFlow

@dariusclay
Copy link
Member

I love porting until I realize my commit history is lost. 🤣

// Azure DevOps environment variables: https://learn.microsoft.com/azure/devops/pipelines/build/variables#build-variables-devops-services
internal static string? AzureBuildId = Environment.GetEnvironmentVariable("Build_BuildId");
internal static string? AzureBuildNumber = Environment.GetEnvironmentVariable("Build_BuildNumber");
internal static string? AzureSourceBranchName = Environment.GetEnvironmentVariable("Build_SourceBranchName");
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Couldn't this alternatively be fetched by the InitializeSourceControlInformation and related targets in MSBuild that are used for SourceLink, that way this would work for local builds as well as CI.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

updated to fetch data from MSBuild properties, which by default are initialized from environment variables.

Copy link

@KalleOlaviNiemitalo KalleOlaviNiemitalo left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What's the purpose of the empty src/Libraries/Microsoft.Extensions.AmbientMetadata.Build/Microsoft.Extensions.AmbientMetadata.Build.json file?

@evgenyfedorov2
Copy link
Member Author

What's the purpose of the empty src/Libraries/Microsoft.Extensions.AmbientMetadata.Build/Microsoft.Extensions.AmbientMetadata.Build.json file?

Removed :)

@evgenyfedorov2 evgenyfedorov2 marked this pull request as ready for review September 16, 2025 13:06
@evgenyfedorov2 evgenyfedorov2 requested a review from a team as a code owner September 16, 2025 13:06
@Copilot Copilot AI review requested due to automatic review settings September 16, 2025 13:06
Copy link
Contributor

@Copilot Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull Request Overview

This PR introduces a new AmbientMetadata.Build component that provides automatic build metadata collection from CI/CD pipelines. The component uses source generation to extract build information at compile time and register it in the dependency injection container as IOptions.

Key changes:

  • New BuildMetadata class with properties for build ID, number, branch name, and source version
  • Source generator that reads MSBuild properties and generates configuration extensions
  • Support for both Azure DevOps and GitHub Actions CI environments

Reviewed Changes

Copilot reviewed 20 out of 24 changed files in this pull request and generated 2 comments.

Show a summary per file
File Description
src/Libraries/Microsoft.Extensions.AmbientMetadata.Build/BuildMetadata.cs Defines the core data model for build metadata
src/Generators/Microsoft.Gen.BuildMetadata/BuildMetadataGenerator.cs Main source generator that creates build metadata extensions
src/Libraries/Microsoft.Extensions.AmbientMetadata.Build/BuildMetadataServiceCollectionExtensions.cs Extension methods for registering BuildMetadata in DI container
src/Libraries/Microsoft.Extensions.AmbientMetadata.Build/buildTransitive/Microsoft.Extensions.AmbientMetadata.Build.props MSBuild properties file that maps CI environment variables to build properties
test/Libraries/Microsoft.Extensions.AmbientMetadata.Build.Tests/ Unit tests for the build metadata functionality

Tip: Customize your code reviews with copilot-instructions.md. Create the file or learn how to get started.

…s/ConfigurationBindingQuirkBehaviorTests.cs

Co-authored-by: Copilot <[email protected]>
Copy link
Contributor

@Copilot Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull Request Overview

Copilot reviewed 20 out of 24 changed files in this pull request and generated 2 comments.


Tip: Customize your code reviews with copilot-instructions.md. Create the file or learn how to get started.

@evgenyfedorov2 evgenyfedorov2 force-pushed the users/evgenyfedorov2/add_build_metadata branch from 9e1009a to 7ed6565 Compare September 16, 2025 13:15
Copy link
Contributor

@Copilot Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull Request Overview

Copilot reviewed 20 out of 24 changed files in this pull request and generated 2 comments.


Tip: Customize your code reviews with copilot-instructions.md. Create the file or learn how to get started.

@jaredpar
Copy link
Member

@333fred, @jjonescz, @JoeRobich PTAL

</PropertyGroup>

<ItemGroup>
<PackageReference Include="Microsoft.CodeAnalysis.CSharp.Workspaces" PrivateAssets="all" />
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Generators should not reference this package; if you accidentally depend on anything from the workspaces layer, you will throw during dotnet build.

Suggested change
<PackageReference Include="Microsoft.CodeAnalysis.CSharp.Workspaces" PrivateAssets="all" />
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" PrivateAssets="all" />

Comment on lines +58 to +75
private readonly struct BuildMetadata
{
public string? BuildId { get; }

public string? BuildNumber { get; }

public string? SourceBranchName { get; }

public string? SourceVersion { get; }

public BuildMetadata(string? buildId, string? buildNumber, string? sourceBranchName, string? sourceVersion)
{
BuildId = buildId;
BuildNumber = buildNumber;
SourceBranchName = sourceBranchName;
SourceVersion = sourceVersion;
}
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Consider just making this a record struct. It provides a more efficient equality definition and is simpler to declare.

Suggested change
private readonly struct BuildMetadata
{
public string? BuildId { get; }
public string? BuildNumber { get; }
public string? SourceBranchName { get; }
public string? SourceVersion { get; }
public BuildMetadata(string? buildId, string? buildNumber, string? sourceBranchName, string? sourceVersion)
{
BuildId = buildId;
BuildNumber = buildNumber;
SourceBranchName = sourceBranchName;
SourceVersion = sourceVersion;
}
}
private readonly record struct BuildMetadata(string? BuildId, string? BuildNumber, string? SourceBranchName, string? SourceVersion);

Comment on lines +37 to +59
OutLn("private sealed class BuildMetadataSource : IConfigurationSource");
OutOpenBrace();
OutLn("public string SectionName { get; }");
OutLn();

OutLn("public BuildMetadataSource(string sectionName)");
OutOpenBrace();
OutNullGuards(checkBuilder: false);
OutLn("SectionName = sectionName;");
OutCloseBrace();
OutLn();

OutLn("public IConfigurationProvider Build(IConfigurationBuilder builder)");
OutOpenBrace();
OutLn("return new MemoryConfigurationProvider(new MemoryConfigurationSource())");
OutOpenBrace();
OutLn($$"""{ $"{SectionName}:buildid", "{{_buildId}}" },""");
OutLn($$"""{ $"{SectionName}:buildnumber", "{{_buildNumber}}" },""");
OutLn($$"""{ $"{SectionName}:sourcebranchname", "{{_sourceBranchName}}" },""");
OutLn($$"""{ $"{SectionName}:sourceversion", "{{_sourceVersion}}" },""");
OutCloseBraceWithExtra(";");
OutCloseBrace();
OutCloseBrace();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Consider putting actual braces in your code; it will allow you to get rid of the formatting suppression and make the formatter generally work with you.

Suggested change
OutLn("private sealed class BuildMetadataSource : IConfigurationSource");
OutOpenBrace();
OutLn("public string SectionName { get; }");
OutLn();
OutLn("public BuildMetadataSource(string sectionName)");
OutOpenBrace();
OutNullGuards(checkBuilder: false);
OutLn("SectionName = sectionName;");
OutCloseBrace();
OutLn();
OutLn("public IConfigurationProvider Build(IConfigurationBuilder builder)");
OutOpenBrace();
OutLn("return new MemoryConfigurationProvider(new MemoryConfigurationSource())");
OutOpenBrace();
OutLn($$"""{ $"{SectionName}:buildid", "{{_buildId}}" },""");
OutLn($$"""{ $"{SectionName}:buildnumber", "{{_buildNumber}}" },""");
OutLn($$"""{ $"{SectionName}:sourcebranchname", "{{_sourceBranchName}}" },""");
OutLn($$"""{ $"{SectionName}:sourceversion", "{{_sourceVersion}}" },""");
OutCloseBraceWithExtra(";");
OutCloseBrace();
OutCloseBrace();
OutLn("private sealed class BuildMetadataSource : IConfigurationSource");
OutOpenBrace();
{
OutLn("public string SectionName { get; }");
OutLn();
OutLn("public BuildMetadataSource(string sectionName)");
OutOpenBrace();
{
OutNullGuards(checkBuilder: false);
OutLn("SectionName = sectionName;");
}
OutCloseBrace();
OutLn();
OutLn("public IConfigurationProvider Build(IConfigurationBuilder builder)");
OutOpenBrace();
{
OutLn("return new MemoryConfigurationProvider(new MemoryConfigurationSource())");
OutOpenBrace();
{
OutLn($$"""{ $"{SectionName}:buildid", "{{_buildId}}" },""");
OutLn($$"""{ $"{SectionName}:buildnumber", "{{_buildNumber}}" },""");
OutLn($$"""{ $"{SectionName}:sourcebranchname", "{{_sourceBranchName}}" },""");
OutLn($$"""{ $"{SectionName}:sourceversion", "{{_sourceVersion}}" },""");
}
OutCloseBraceWithExtra(";");
}
OutCloseBrace();
}
OutCloseBrace();

private void GenerateBuildMetadataSource()
{
OutGeneratedCodeAttribute();
OutLn("[EditorBrowsable(EditorBrowsableState.Never)]");
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Consider using global::Fully.Prefixed.Types, rather than relying on usings. You don't know what global usings the user will have set up, and names they may have defined could potentially conflict with yours. Smaller generators can get away without fully-specifying, but for reliability we suggest that larger generators always use the fully-qualified form.


namespace Microsoft.Gen.BuildMetadata;

internal sealed class Emitter : EmitterBase
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

GeneratorUtilies.cs is not in this review, so I can't comment on it, but it links an issue that was closed in 2021. Consider removing the link and using a complete reason.

string source,
TestAnalyzerConfigOptionsProvider optionsProvider)
{
// Create a test project and compilation
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Consider using Microsoft.CodeAnalysis.CSharp.SourceGenerators.Testing, rather than a homegrown framework. Specifically, RoslynTestUtils appears to have a number of anti-patterns, such as getting assemblies by using typeof(object).Location. The official testing library takes care of a lot of this for you, getting the real assemblies for different .NET versions.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

6 participants