diff --git a/.github/workflows/check-sdk-versions.yml b/.github/workflows/check-sdk-versions.yml
new file mode 100644
index 0000000000..5a524c2fa7
--- /dev/null
+++ b/.github/workflows/check-sdk-versions.yml
@@ -0,0 +1,24 @@
+name: SdkVersionCheck
+
+on:
+ push:
+ branches: [ main ]
+ pull_request:
+ merge_group:
+ workflow_dispatch:
+
+jobs:
+ check-sdk-versions:
+ runs-on: windows-latest
+ steps:
+
+ - name: Checkout
+ uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # tag: v4.2.1
+
+ - name: Setup .NET 8
+ uses: actions/setup-dotnet@6bd8b7f7774af54e05809fcc5431931b3eb1ddee # tag: v4.0.1
+ with:
+ dotnet-version: 8.0.403
+
+ - name: Run VerifySdkVersions
+ run: ./build.cmd VerifySdkVersions
diff --git a/OpenTelemetry.AutoInstrumentation.sln b/OpenTelemetry.AutoInstrumentation.sln
index 225ad7d5c7..39de683c42 100644
--- a/OpenTelemetry.AutoInstrumentation.sln
+++ b/OpenTelemetry.AutoInstrumentation.sln
@@ -243,6 +243,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "TestApplication.RabbitMq",
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TestApplication.Owin.IIS.NetFramework", "test\test-applications\integrations\TestApplication.Owin.IIS.NetFramework\TestApplication.Owin.IIS.NetFramework.csproj", "{AA3E0C5C-A4E2-46AB-BD18-2D30D3ABF692}"
EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SdkVersionVerifier", "tools\SdkVersionVerifier\SdkVersionVerifier.csproj", "{C75FA076-D460-414B-97F7-6F8D0E85AE74}"
+EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@@ -1495,6 +1497,22 @@ Global
{AA3E0C5C-A4E2-46AB-BD18-2D30D3ABF692}.Release|x64.Build.0 = Release|Any CPU
{AA3E0C5C-A4E2-46AB-BD18-2D30D3ABF692}.Release|x86.ActiveCfg = Release|Any CPU
{AA3E0C5C-A4E2-46AB-BD18-2D30D3ABF692}.Release|x86.Build.0 = Release|Any CPU
+ {C75FA076-D460-414B-97F7-6F8D0E85AE74}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {C75FA076-D460-414B-97F7-6F8D0E85AE74}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {C75FA076-D460-414B-97F7-6F8D0E85AE74}.Debug|ARM64.ActiveCfg = Debug|Any CPU
+ {C75FA076-D460-414B-97F7-6F8D0E85AE74}.Debug|ARM64.Build.0 = Debug|Any CPU
+ {C75FA076-D460-414B-97F7-6F8D0E85AE74}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {C75FA076-D460-414B-97F7-6F8D0E85AE74}.Debug|x64.Build.0 = Debug|Any CPU
+ {C75FA076-D460-414B-97F7-6F8D0E85AE74}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {C75FA076-D460-414B-97F7-6F8D0E85AE74}.Debug|x86.Build.0 = Debug|Any CPU
+ {C75FA076-D460-414B-97F7-6F8D0E85AE74}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {C75FA076-D460-414B-97F7-6F8D0E85AE74}.Release|Any CPU.Build.0 = Release|Any CPU
+ {C75FA076-D460-414B-97F7-6F8D0E85AE74}.Release|ARM64.ActiveCfg = Release|Any CPU
+ {C75FA076-D460-414B-97F7-6F8D0E85AE74}.Release|ARM64.Build.0 = Release|Any CPU
+ {C75FA076-D460-414B-97F7-6F8D0E85AE74}.Release|x64.ActiveCfg = Release|Any CPU
+ {C75FA076-D460-414B-97F7-6F8D0E85AE74}.Release|x64.Build.0 = Release|Any CPU
+ {C75FA076-D460-414B-97F7-6F8D0E85AE74}.Release|x86.ActiveCfg = Release|Any CPU
+ {C75FA076-D460-414B-97F7-6F8D0E85AE74}.Release|x86.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
@@ -1583,6 +1601,7 @@ Global
{959764E7-5A0C-4511-8004-48DE6B10F499} = {3F051815-8E0D-4356-BC36-55CA642DDF18}
{91D883EC-069E-46BC-B6F7-67C94299851E} = {E409ADD3-9574-465C-AB09-4324D205CC7C}
{AA3E0C5C-A4E2-46AB-BD18-2D30D3ABF692} = {E409ADD3-9574-465C-AB09-4324D205CC7C}
+ {C75FA076-D460-414B-97F7-6F8D0E85AE74} = {00F4C92D-6652-4BD8-A334-B35D3E711BE6}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {160A1D00-1F5B-40F8-A155-621B4459D78F}
diff --git a/build/Build.Steps.cs b/build/Build.Steps.cs
index ebf6785c13..a9bf13db66 100644
--- a/build/Build.Steps.cs
+++ b/build/Build.Steps.cs
@@ -362,6 +362,16 @@ void RemoveFilesInNetFolderAvailableInAdditionalStore()
.DependsOn(PublishNativeProfilerLinux)
.DependsOn(PublishNativeProfilerMacOs);
+ Target VerifySdkVersions => _ => _
+ .Executes(() =>
+ {
+ var verifier = Solution.GetProjectByName(Projects.Tools.SdkVersionVerifierTool);
+
+ DotNetRun(s => s
+ .SetProjectFile(verifier)
+ .SetApplicationArguments(RootDirectory));
+ });
+
Target GenerateLibraryVersionFiles => _ => _
.After(PublishManagedProfiler)
.Executes(() =>
diff --git a/build/Projects.cs b/build/Projects.cs
index 8d47c3841a..c10e979376 100644
--- a/build/Projects.cs
+++ b/build/Projects.cs
@@ -35,5 +35,6 @@ public static class Tools
{
public const string LibraryVersionsGenerator = "LibraryVersionsGenerator";
public const string GacInstallTool = "GacInstallTool";
+ public const string SdkVersionVerifierTool = "SdkVersionVerifier";
}
}
diff --git a/tools/Directory.Packages.props b/tools/Directory.Packages.props
index 7d46eb033d..6c45697627 100644
--- a/tools/Directory.Packages.props
+++ b/tools/Directory.Packages.props
@@ -1,6 +1,7 @@
+
@@ -9,5 +10,6 @@
+
\ No newline at end of file
diff --git a/tools/SdkVersionVerifier/ActionWorkflowVerifier.cs b/tools/SdkVersionVerifier/ActionWorkflowVerifier.cs
new file mode 100644
index 0000000000..4aea998287
--- /dev/null
+++ b/tools/SdkVersionVerifier/ActionWorkflowVerifier.cs
@@ -0,0 +1,118 @@
+// Copyright The OpenTelemetry Authors
+// SPDX-License-Identifier: Apache-2.0
+
+using YamlDotNet.RepresentationModel;
+
+namespace SdkVersionVerifier;
+
+internal static class ActionWorkflowVerifier
+{
+ public static DotnetSdkVersion? GetExpectedSdkVersionFromSampleWorkflow(string root)
+ {
+ var defaultWorkflow = Path.Combine(GetWorkflowsDirectory(root), "build.yml");
+ return ExtractDotnetSdkVersions(File.ReadAllText(defaultWorkflow)).FirstOrDefault();
+ }
+
+ public static bool VerifyVersions(string root, DotnetSdkVersion expectedDotnetSdkVersion)
+ {
+ var workflowsDir = GetWorkflowsDirectory(root);
+ var workflows = Directory.GetFiles(workflowsDir, "*.yml");
+
+ return FileVerifier.VerifyMultiple(workflows, VerifySdkVersions, expectedDotnetSdkVersion);
+ }
+
+ private static string GetWorkflowsDirectory(string root)
+ {
+ return Path.Combine(root, ".github", "workflows");
+ }
+
+ private static bool VerifySdkVersions(string content, DotnetSdkVersion expectedDotnetSdkVersion)
+ {
+ foreach (var extractedSdkVersion in ExtractDotnetSdkVersions(content))
+ {
+ if (!VersionComparer.CompareVersions(expectedDotnetSdkVersion, extractedSdkVersion))
+ {
+ return false;
+ }
+ }
+
+ return true;
+ }
+
+ private static IEnumerable ExtractDotnetSdkVersions(string content)
+ {
+ var workflow = new YamlStream();
+ using var stringReader = new StringReader(content);
+ workflow.Load(stringReader);
+
+ var jobs = ExtractJobs(workflow);
+ foreach (var job in jobs)
+ {
+ if (!job.Children.TryGetValue(new YamlScalarNode("steps"), out var stepsNode))
+ {
+ continue;
+ }
+
+ foreach (var step in (YamlSequenceNode)stepsNode)
+ {
+ var jobStepNode = (YamlMappingNode)step;
+ if (jobStepNode.Children.TryGetValue(new YamlScalarNode("uses"), out var usesNode) && usesNode.ToString().StartsWith("actions/setup-dotnet"))
+ {
+ var withNode = (YamlMappingNode)jobStepNode.Children[new YamlScalarNode("with")];
+ var dotnetVersionNode = (YamlScalarNode)withNode.Children[new YamlScalarNode("dotnet-version")];
+
+ var extractedVersion = ExtractVersion(dotnetVersionNode);
+ if (extractedVersion is not null)
+ {
+ yield return extractedVersion;
+ }
+ }
+ }
+ }
+ }
+
+ private static DotnetSdkVersion? ExtractVersion(YamlScalarNode dotnetVersionNode)
+ {
+ // Extract versions from the node value e.g.:
+ // dotnet-version: |
+ // 6.0.427
+ // 7.0.410
+ // 8.0.403
+
+ string? sdk6Version = null;
+ string? sdk7Version = null;
+ string? sdk8Version = null;
+
+ foreach (var version in dotnetVersionNode.ToString().Split())
+ {
+ if (version.StartsWith('6'))
+ {
+ sdk6Version = version;
+ }
+
+ if (version.StartsWith('7'))
+ {
+ sdk7Version = version;
+ }
+
+ if (version.StartsWith('8'))
+ {
+ sdk8Version = version;
+ }
+ }
+
+ if (sdk6Version is not null || sdk7Version is not null || sdk8Version is not null)
+ {
+ return new DotnetSdkVersion(sdk6Version, sdk7Version, sdk8Version);
+ }
+
+ return null;
+ }
+
+ private static IEnumerable ExtractJobs(YamlStream yaml)
+ {
+ var rootNode = yaml.Documents[0].RootNode as YamlMappingNode;
+ var jobsNode = (YamlMappingNode)rootNode!.Children[new YamlScalarNode("jobs")];
+ return jobsNode.Children.Select(j => (YamlMappingNode)j.Value);
+ }
+}
diff --git a/tools/SdkVersionVerifier/DockerfileVerifier.cs b/tools/SdkVersionVerifier/DockerfileVerifier.cs
new file mode 100644
index 0000000000..a6e7122abe
--- /dev/null
+++ b/tools/SdkVersionVerifier/DockerfileVerifier.cs
@@ -0,0 +1,63 @@
+// Copyright The OpenTelemetry Authors
+// SPDX-License-Identifier: Apache-2.0
+
+using System.Text.RegularExpressions;
+using dockerfile;
+
+namespace SdkVersionVerifier;
+
+internal static partial class DockerfileVerifier
+{
+ public static bool VerifyVersions(string root, DotnetSdkVersion expectedDotnetSdkVersion)
+ {
+ var dockerfilesDir = Path.Combine(root, "docker");
+ var dockerfiles = Directory.GetFiles(dockerfilesDir, "*.dockerfile");
+
+ return FileVerifier.VerifyMultiple(dockerfiles, VerifyVersionsFromDockerfiles, expectedDotnetSdkVersion);
+ }
+
+ [GeneratedRegex(@"-v (\d\.\d\.\d{3})\s", RegexOptions.IgnoreCase, "en-US")]
+ internal static partial Regex VersionRegex();
+
+ private static bool VerifyVersionsFromDockerfiles(string content, DotnetSdkVersion expectedDotnetSdkVersion)
+ {
+ using var stringReader = new StringReader(content);
+ var dockerfile = Dockerfile.Parse(stringReader);
+
+ string? net6SdkVersion = null;
+ string? net7SdkVersion = null;
+ string? net8SdkVersion = null;
+
+ foreach (var instruction in dockerfile.Instructions.Where(i => i.Arguments.Contains("./dotnet-install.sh")))
+ {
+ // Extract version from line like `&& ./dotnet-install.sh -v 6.0.427 --install-dir /usr/share/dotnet --no-path \`
+ var result = VersionRegex().Match(instruction.Arguments);
+ if (!result.Success)
+ {
+ continue;
+ }
+
+ var extractedSdkVersion = result.Groups[1].Value;
+ if (extractedSdkVersion.StartsWith('6'))
+ {
+ net6SdkVersion = extractedSdkVersion;
+ }
+ else if (extractedSdkVersion.StartsWith('7'))
+ {
+ net7SdkVersion = extractedSdkVersion;
+ }
+ }
+
+ // Extract NET8 SDK version from the base image tag
+ // e.g. FROM mcr.microsoft.com/dotnet/sdk:8.0.403-alpine3.20
+ var fromInstruction = dockerfile.Instructions
+ .SingleOrDefault(i => i.InstructionName == "FROM" && i.Arguments.StartsWith("mcr.microsoft.com/dotnet/sdk"));
+
+ if (fromInstruction is not null)
+ {
+ net8SdkVersion = fromInstruction.Arguments.Split(':')[1].Split('-')[0];
+ }
+
+ return VersionComparer.CompareVersions(expectedDotnetSdkVersion, net6SdkVersion, net7SdkVersion, net8SdkVersion);
+ }
+}
diff --git a/tools/SdkVersionVerifier/DotnetSdkVersion.cs b/tools/SdkVersionVerifier/DotnetSdkVersion.cs
new file mode 100644
index 0000000000..67976564ed
--- /dev/null
+++ b/tools/SdkVersionVerifier/DotnetSdkVersion.cs
@@ -0,0 +1,6 @@
+// Copyright The OpenTelemetry Authors
+// SPDX-License-Identifier: Apache-2.0
+
+namespace SdkVersionVerifier;
+
+internal record DotnetSdkVersion(string? Net6SdkVersion, string? Net7SdkVersion, string? Net8SdkVersion);
diff --git a/tools/SdkVersionVerifier/FileVerifier.cs b/tools/SdkVersionVerifier/FileVerifier.cs
new file mode 100644
index 0000000000..77c92d0ed5
--- /dev/null
+++ b/tools/SdkVersionVerifier/FileVerifier.cs
@@ -0,0 +1,29 @@
+// Copyright The OpenTelemetry Authors
+// SPDX-License-Identifier: Apache-2.0
+
+namespace SdkVersionVerifier;
+
+internal static class FileVerifier
+{
+ public static bool VerifyMultiple(
+ IEnumerable filePaths,
+ Func versionPredicate,
+ DotnetSdkVersion dotnetSdkVersion)
+ {
+ foreach (var filePath in filePaths)
+ {
+ Console.WriteLine($"Verifying SDK versions from {filePath}");
+ var content = File.ReadAllText(filePath);
+ var versionsMatch = versionPredicate(content, dotnetSdkVersion);
+ if (versionsMatch)
+ {
+ continue;
+ }
+
+ Console.WriteLine($"Invalid SDK versions in {filePath}");
+ return false;
+ }
+
+ return true;
+ }
+}
diff --git a/tools/SdkVersionVerifier/Program.cs b/tools/SdkVersionVerifier/Program.cs
new file mode 100644
index 0000000000..c49fe9587c
--- /dev/null
+++ b/tools/SdkVersionVerifier/Program.cs
@@ -0,0 +1,43 @@
+// Copyright The OpenTelemetry Authors
+// SPDX-License-Identifier: Apache-2.0
+
+namespace SdkVersionVerifier;
+
+internal static class Program
+{
+ public static int Main(string[] args)
+ {
+ if (args.Length != 1)
+ {
+ Console.WriteLine("Invalid arguments. Single argument with repository root is required.");
+ return 1;
+ }
+
+ var directoryRoot = args[0];
+
+ // Set expected dotnet SDK versions based on sample workflow.
+ // This set of versions will be expected to be used consistently
+ // in GitHub actions workflows and dockerfiles.
+ var expectedVersion = ActionWorkflowVerifier.GetExpectedSdkVersionFromSampleWorkflow(directoryRoot);
+ if (expectedVersion is null)
+ {
+ Console.WriteLine("Unable to extract expected SDK version from sample workflow file.");
+ return 1;
+ }
+
+ Console.WriteLine($"Expected SDK versions: {expectedVersion}");
+ if (!ActionWorkflowVerifier.VerifyVersions(directoryRoot, expectedVersion))
+ {
+ Console.WriteLine("Invalid SDK versions in GitHub actions workflows.");
+ return 1;
+ }
+
+ if (!DockerfileVerifier.VerifyVersions(directoryRoot, expectedVersion))
+ {
+ Console.WriteLine("Invalid SDK versions in dockerfiles.");
+ return 1;
+ }
+
+ return 0;
+ }
+}
diff --git a/tools/SdkVersionVerifier/Properties/launchSettings.json b/tools/SdkVersionVerifier/Properties/launchSettings.json
new file mode 100644
index 0000000000..789cc1e9ca
--- /dev/null
+++ b/tools/SdkVersionVerifier/Properties/launchSettings.json
@@ -0,0 +1,11 @@
+{
+ "$schema": "http://json.schemastore.org/launchsettings.json",
+ "profiles": {
+ "SdkVersionVerifier": {
+ "commandName": "Project",
+ "commandLineArgs": "$(SolutionDir)",
+ "environmentVariables": {
+ }
+ }
+ }
+}
diff --git a/tools/SdkVersionVerifier/SdkVersionVerifier.csproj b/tools/SdkVersionVerifier/SdkVersionVerifier.csproj
new file mode 100644
index 0000000000..93d82ca92b
--- /dev/null
+++ b/tools/SdkVersionVerifier/SdkVersionVerifier.csproj
@@ -0,0 +1,15 @@
+
+
+
+ net8.0
+ enable
+ enable
+ Exe
+
+
+
+
+
+
+
+
diff --git a/tools/SdkVersionVerifier/VersionComparer.cs b/tools/SdkVersionVerifier/VersionComparer.cs
new file mode 100644
index 0000000000..e737228e5e
--- /dev/null
+++ b/tools/SdkVersionVerifier/VersionComparer.cs
@@ -0,0 +1,36 @@
+// Copyright The OpenTelemetry Authors
+// SPDX-License-Identifier: Apache-2.0
+
+namespace SdkVersionVerifier;
+
+internal static class VersionComparer
+{
+ public static bool CompareVersions(DotnetSdkVersion expectedDotnetSdkVersion, DotnetSdkVersion version)
+ {
+ return CompareVersions(expectedDotnetSdkVersion, version.Net6SdkVersion, version.Net7SdkVersion, version.Net8SdkVersion);
+ }
+
+ public static bool CompareVersions(
+ DotnetSdkVersion expectedDotnetSdkVersion,
+ string? net6SdkVersion,
+ string? net7SdkVersion,
+ string? net8SdkVersion)
+ {
+ return CompareVersion(expectedDotnetSdkVersion.Net6SdkVersion!, net6SdkVersion) &&
+ CompareVersion(expectedDotnetSdkVersion.Net7SdkVersion!, net7SdkVersion) &&
+ CompareVersion(expectedDotnetSdkVersion.Net8SdkVersion!, net8SdkVersion);
+ }
+
+ private static bool CompareVersion(string expectedVersion, string? extractedVersion)
+ {
+ // For some of the workflows, singular version of SDK is used
+ // e.g. in dotnet-format.yml, so extracted version might be missing.
+ if (extractedVersion is not null && extractedVersion != expectedVersion)
+ {
+ Console.WriteLine($".NET SDK Version mismatch: expected={expectedVersion}, actual={extractedVersion}");
+ return false;
+ }
+
+ return true;
+ }
+}