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; + } +}