diff --git a/src/SourceBuild/content/.vault-config/vmr-pipeline-secrets.yaml b/src/SourceBuild/content/.vault-config/vmr-pipeline-secrets.yaml new file mode 100644 index 000000000..643a89ba8 --- /dev/null +++ b/src/SourceBuild/content/.vault-config/vmr-pipeline-secrets.yaml @@ -0,0 +1,13 @@ +# Partially copied from https://github.com/dotnet/arcade/blob/dfc6882da43decb37f12e0d9011ce82b25225578/.vault-config/product-builds-dnceng-pipeline-secrets.yaml + +secrets: + BotAccount-dotnet-sb-bot: + type: github-account + parameters: + Name: dotnet-sb-bot + + BotAccount-dotnet-sb-bot-pat: + type: github-access-token + parameters: + gitHubBotAccountSecret: BotAccount-dotnet-sb-bot + gitHubBotAccountName: dotnet-sb-bot \ No newline at end of file diff --git a/src/SourceBuild/content/eng/pipelines/source-build-sdk-diff-tests.yml b/src/SourceBuild/content/eng/pipelines/source-build-sdk-diff-tests.yml index 6704e6a0b..6642c705b 100644 --- a/src/SourceBuild/content/eng/pipelines/source-build-sdk-diff-tests.yml +++ b/src/SourceBuild/content/eng/pipelines/source-build-sdk-diff-tests.yml @@ -33,6 +33,9 @@ parameters: variables: - template: /src/installer/eng/pipelines/templates/variables/vmr-build.yml@self +# GH access token for SB bot - BotAccount-dotnet-sb-bot-pat +- group: DotNet-Source-Build-Bot-Secrets-MVP + jobs: - template: templates/jobs/sdk-diff-tests.yml parameters: @@ -40,6 +43,7 @@ jobs: targetRid: ${{ variables.centOSStreamX64Rid }} architecture: x64 dotnetDotnetRunId: ${{ parameters.dotnetDotnetRunId }} + publishTestResultsPr: true - template: templates/jobs/sdk-diff-tests.yml parameters: diff --git a/src/SourceBuild/content/eng/pipelines/templates/jobs/sdk-diff-tests.yml b/src/SourceBuild/content/eng/pipelines/templates/jobs/sdk-diff-tests.yml index 52435dbc0..f885202e8 100644 --- a/src/SourceBuild/content/eng/pipelines/templates/jobs/sdk-diff-tests.yml +++ b/src/SourceBuild/content/eng/pipelines/templates/jobs/sdk-diff-tests.yml @@ -11,6 +11,10 @@ parameters: - name: dotnetDotnetRunId type: string +- name: publishTestResultsPr + type: boolean + default: false + jobs: - job: ${{ parameters.buildName }}_${{ parameters.architecture }} timeoutInMinutes: 150 @@ -161,3 +165,12 @@ jobs: mergeTestResults: true publishRunAttachments: true testRunTitle: $(Agent.JobName) + + - ${{ if and(eq(parameters.publishTestResultsPr, 'true'), or(eq(variables['Build.SourceBranch'], 'refs/heads/main'), startsWith(variables['Build.SourceBranch'], 'refs/heads/release'))) }}: + - template: ../steps/create-baseline-update-pr.yml + parameters: + pipeline: sdk + repo: dotnet/installer + originalFilesDirectory: src/SourceBuild/content/test/Microsoft.DotNet.SourceBuild.SmokeTests/assets/baselines + updatedFilesDirectory: $(Build.StagingDirectory)/BuildLogs + pullRequestTitle: Update Source-Build SDK Diff Tests Baselines and Exclusions diff --git a/src/SourceBuild/content/eng/pipelines/templates/steps/create-baseline-update-pr.yml b/src/SourceBuild/content/eng/pipelines/templates/steps/create-baseline-update-pr.yml new file mode 100644 index 000000000..3c53c051c --- /dev/null +++ b/src/SourceBuild/content/eng/pipelines/templates/steps/create-baseline-update-pr.yml @@ -0,0 +1,48 @@ +parameters: +# The pipeline that is being run +# Used to determine the correct baseline update tool to run +# Currently only supports "sdk" and "license" +- name: pipeline + type: string + +# The GitHub repository to create the PR in. +# Should be in the form '/' +- name: repo + type: string + +# Path to the directory containing the original test files +# Should be relative to the "repo" parameter +- name: originalFilesDirectory + type: string + +# Path to the directory containing the updated test files +# Should be absolute or relative to the working directory of the tool +- name: updatedFilesDirectory + type: string + +- name: pullRequestTitle + type: string + default: Update Test Baselines + +steps: + - script: | + restoreSources="https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-public/nuget/v3/index.json" + restoreSources+="%3Bhttps://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-libraries/nuget/v3/index.json" + + branchName=$(echo "$(Build.SourceBranch)" | sed 's/refs\/heads\///g') + + .dotnet/dotnet run \ + --project eng/tools/CreateBaselineUpdatePR/ \ + --property:RestoreSources="$restoreSources" \ + "${{ parameters.pipeline }}" \ + "${{ parameters.repo }}" \ + "${{ parameters.originalFilesDirectory }}" \ + "${{ parameters.updatedFilesDirectory }}" \ + "$(Build.BuildId)" \ + --title "${{ parameters.pullRequestTitle }}" \ + --branch "$branchName" + displayName: Publish Test Results PR + workingDirectory: $(Build.SourcesDirectory) + condition: succeededOrFailed() + env: + GH_TOKEN: $(BotAccount-dotnet-sb-bot-pat) diff --git a/src/SourceBuild/content/eng/pipelines/vmr-license-scan.yml b/src/SourceBuild/content/eng/pipelines/vmr-license-scan.yml index ca70fd70c..d00e6450e 100644 --- a/src/SourceBuild/content/eng/pipelines/vmr-license-scan.yml +++ b/src/SourceBuild/content/eng/pipelines/vmr-license-scan.yml @@ -20,7 +20,10 @@ parameters: default: " " # Set it to an empty string to allow it be an optional parameter variables: - installerRoot: '$(Build.SourcesDirectory)/src/installer' +# GH access token for SB bot - BotAccount-dotnet-sb-bot-pat +- group: DotNet-Source-Build-Bot-Secrets-MVP +- name: installerRoot + value: '$(Build.SourcesDirectory)/src/installer' jobs: - job: Setup @@ -136,3 +139,45 @@ jobs: mergeTestResults: true publishRunAttachments: true testRunTitle: $(Agent.JobName) + +- job: CreateBaselineUpdatePR + dependsOn: LicenseScan + condition: or(eq(variables['Build.SourceBranch'], 'refs/heads/main'), startsWith(variables['Build.SourceBranch'], 'refs/heads/release')) + pool: + name: NetCore1ESPool-Svc-Internal + demands: ImageOverride -equals 1es-ubuntu-2004 + variables: + - template: templates/variables/pipelines.yml + steps: + + - script: | + source ./eng/common/tools.sh + InitializeDotNetCli true + displayName: Install .NET SDK + workingDirectory: $(Build.SourcesDirectory) + + - task: DownloadPipelineArtifact@2 + inputs: + buildType: specific + buildVersionToDownload: specific + project: internal + buildId: $(Build.BuildId) + artifact: '' + patterns: '**/Updated*' + allowPartiallySucceededBuilds: true + allowFailedBuilds: true + downloadPath: $(Pipeline.Workspace)/Artifacts + checkDownloadedFiles: true + displayName: Download Updated Baselines + + - script: | + find $(Pipeline.Workspace)/Artifacts -type f -exec mv {} $(Pipeline.Workspace)/Artifacts \; + displayName: Move Artifacts to root + + - template: templates/steps/create-baseline-update-pr.yml + parameters: + pipeline: license + repo: dotnet/installer + originalFilesDirectory: src/SourceBuild/content/test/Microsoft.DotNet.SourceBuild.SmokeTests/assets/baselines/licenses + updatedFilesDirectory: $(Pipeline.Workspace)/Artifacts + pullRequestTitle: Update Source-Build License Scan Baselines and Exclusions diff --git a/src/SourceBuild/content/eng/tools/CreateBaselineUpdatePR/CreateBaselineUpdatePR.csproj b/src/SourceBuild/content/eng/tools/CreateBaselineUpdatePR/CreateBaselineUpdatePR.csproj new file mode 100644 index 000000000..f1d4d70fa --- /dev/null +++ b/src/SourceBuild/content/eng/tools/CreateBaselineUpdatePR/CreateBaselineUpdatePR.csproj @@ -0,0 +1,17 @@ + + + + Exe + net8.0 + enable + enable + + + + + + + + + + diff --git a/src/SourceBuild/content/eng/tools/CreateBaselineUpdatePR/Log.cs b/src/SourceBuild/content/eng/tools/CreateBaselineUpdatePR/Log.cs new file mode 100644 index 000000000..bfca7e600 --- /dev/null +++ b/src/SourceBuild/content/eng/tools/CreateBaselineUpdatePR/Log.cs @@ -0,0 +1,68 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.Extensions.Logging; + +namespace CreateBaselineUpdatePR; + +public static class Log +{ + public static LogLevel Level = LogLevel.Information; + + private static bool WarningLogged = false; + + private static bool ErrorLogged = false; + + private static readonly Lazy _logger = new Lazy(ConfigureLogger); + + public static void LogDebug(string message) + { + _logger.Value.LogDebug(message); + } + + public static void LogInformation(string message) + { + _logger.Value.LogInformation(message); + } + + public static void LogWarning(string message) + { + _logger.Value.LogWarning(message); + WarningLogged = true; + } + + public static void LogError(string message) + { + _logger.Value.LogError(message); + ErrorLogged = true; + } + + private static ILogger ConfigureLogger() + { + using ILoggerFactory loggerFactory = + LoggerFactory.Create(builder => + builder.AddSimpleConsole(options => + { + options.SingleLine = true; + options.TimestampFormat = "HH:mm:ss "; + options.UseUtcTimestamp = true; + }) + .SetMinimumLevel(Level)); + return loggerFactory.CreateLogger(System.Reflection.Assembly.GetExecutingAssembly().GetName().Name!); + } + + public static int GetExitCode() + { + if (ErrorLogged) + { + return 1; + } + + if (WarningLogged) + { + return 2; + } + + return 0; + } +} diff --git a/src/SourceBuild/content/eng/tools/CreateBaselineUpdatePR/PRCreator.cs b/src/SourceBuild/content/eng/tools/CreateBaselineUpdatePR/PRCreator.cs new file mode 100644 index 000000000..8e67ca94b --- /dev/null +++ b/src/SourceBuild/content/eng/tools/CreateBaselineUpdatePR/PRCreator.cs @@ -0,0 +1,310 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace CreateBaselineUpdatePR; + +using Octokit; +using System.Text; +using System.Threading.Tasks; +using System.Collections.Generic; + +public class PRCreator +{ + private readonly string _repoOwner; + private readonly string _repoName; + private readonly GitHubClient _client; + private const string BuildLink = "https://dev.azure.com/dnceng/internal/_build/results?buildId="; + private const string TreeMode = "040000"; + public PRCreator(string repo, string gitHubToken) + { + // Create a new GitHub client + _client = new GitHubClient(new ProductHeaderValue(System.Reflection.Assembly.GetExecutingAssembly().GetName().Name)); + var authToken = new Credentials(gitHubToken); + _client.Credentials = authToken; + _repoOwner = repo.Split('/')[0]; + _repoName = repo.Split('/')[1]; + } + + public async Task ExecuteAsync( + string originalFilesDirectory, + string updatedFilesDirectory, + int buildId, + string title, + string targetBranch, + Pipelines pipeline) + { + DateTime startTime = DateTime.Now.ToUniversalTime(); + + Log.LogInformation($"Starting PR creation at {startTime} UTC for pipeline {pipeline}."); + + var updatedTestsFiles = GetUpdatedFiles(updatedFilesDirectory); + + // Create a new tree for the originalFilesDirectory based on the target branch + var originalTreeResponse = await _client.Git.Tree.GetRecursive(_repoOwner, _repoName, targetBranch); + var testResultsTreeItems = originalTreeResponse.Tree + .Where(file => file.Path.Contains(originalFilesDirectory) && file.Path != originalFilesDirectory) + .Select(file => new NewTreeItem + { + Path = Path.GetRelativePath(originalFilesDirectory, file.Path), + Mode = file.Mode, + Type = file.Type.Value, + Sha = file.Sha + }) + .ToList(); + + // Update the test results tree + testResultsTreeItems = await UpdateAllFilesAsync(updatedTestsFiles, testResultsTreeItems); + var testResultsTreeResponse = await CreateTreeFromItemsAsync(testResultsTreeItems); + var parentTreeResponse = await CreateParentTreeAsync(testResultsTreeResponse, originalTreeResponse, originalFilesDirectory); + + await CreateOrUpdatePullRequestAsync(parentTreeResponse, buildId, title, targetBranch); + + return Log.GetExitCode(); + } + + // Return a dictionary using the filename without the + // "Updated" prefix and anything after the first '.' as the key + private Dictionary> GetUpdatedFiles(string updatedFilesDirectory) => + Directory + .GetFiles(updatedFilesDirectory, "Updated*", SearchOption.AllDirectories) + .GroupBy(updatedTestsFile => ParseUpdatedFileName(updatedTestsFile).Split('.')[0]) + .ToDictionary( + group => group.Key, + group => new HashSet(group) + ); + + private async Task> UpdateAllFilesAsync(Dictionary> updatedFiles, List tree) + { + foreach (var updatedFile in updatedFiles) + { + foreach (var filePath in updatedFile.Value) + { + var content = File.ReadAllText(filePath); + string originalFileName = Path.GetFileName(ParseUpdatedFileName(filePath)); + tree = await UpdateFileAsync(tree, content, originalFileName, originalFileName); + } + } + return tree; + } + + private async Task> UpdateFileAsync(List tree, string content, string searchFileName, string updatedPath) + { + var originalTreeItem = tree + .Where(item => item.Path.Contains(searchFileName)) + .FirstOrDefault(); + + if (originalTreeItem == null) + { + // Path not in the tree, add a new tree item + var blob = await CreateBlobAsync(content); + tree.Add(new NewTreeItem + { + Type = TreeType.Blob, + Mode = FileMode.File, + Path = updatedPath, + Sha = blob.Sha + }); + } + else + { + // Path in the tree, update the sha and the content + var blob = await CreateBlobAsync(content); + originalTreeItem.Sha = blob.Sha; + } + return tree; + } + + private async Task CreateBlobAsync(string content) + { + var blob = new NewBlob + { + Content = content, + Encoding = EncodingType.Utf8 + }; + return await _client.Git.Blob.Create(_repoOwner, _repoName, blob); + } + + private string ParseUpdatedFileName(string updatedFile) => updatedFile.Split("Updated")[1]; + + private async Task CreateTreeFromItemsAsync(List items, string path = "") + { + var newTreeItems = new List(); + + var groups = items.GroupBy(item => Path.GetDirectoryName(item.Path)); + foreach (var group in groups) + { + if (string.IsNullOrEmpty(group.Key) || group.Key == path) + { + // These items are in the current directory, so add them to the new tree items + foreach (var item in group) + { + if(item.Type != TreeType.Tree) + { + newTreeItems.Add(new NewTreeItem + { + Path = path == string.Empty ? item.Path : Path.GetRelativePath(path, item.Path), + Mode = item.Mode, + Type = item.Type, + Sha = item.Sha + }); + } + } + } + else + { + // These items are in a subdirectory, so recursively create a tree for them + var subtreeResponse = await CreateTreeFromItemsAsync(group.ToList(), group.Key); + newTreeItems.Add(new NewTreeItem + { + Path = group.Key, + Mode = TreeMode, + Type = TreeType.Tree, + Sha = subtreeResponse.Sha + }); + } + } + + var newTree = new NewTree(); + foreach (var item in newTreeItems) + { + newTree.Tree.Add(item); + } + return await _client.Git.Tree.Create(_repoOwner, _repoName, newTree); + } + + private async Task CreateParentTreeAsync(TreeResponse testResultsTreeResponse, TreeResponse originalTreeResponse, string originalFilesDirectory) + { + // Create a new tree for the parent directory + NewTree parentTree = new NewTree { BaseTree = originalTreeResponse.Sha }; + + // Connect the updated test results tree + parentTree.Tree.Add(new NewTreeItem + { + Path = originalFilesDirectory, + Mode = TreeMode, + Type = TreeType.Tree, + Sha = testResultsTreeResponse.Sha + }); + + return await _client.Git.Tree.Create(_repoOwner, _repoName, parentTree); + } + + private async Task CreateOrUpdatePullRequestAsync(TreeResponse parentTreeResponse, int buildId, string title, string targetBranch) + { + var existingPullRequest = await GetExistingPullRequestAsync(title, targetBranch); + + // Create the branch name and get the head reference + string newBranchName = string.Empty; + string headSha = await GetHeadShaAsync(targetBranch); + if (existingPullRequest == null) + { + string utcTime = DateTime.UtcNow.ToString("yyyyMMddHHmmss"); + newBranchName = $"pr-baseline-{utcTime}"; + } + else + { + newBranchName = existingPullRequest.Head.Ref; + + try + { + // Merge the target branch into the existing pull request + var merge = new NewMerge(newBranchName, headSha); + await _client.Repository.Merging.Create(_repoOwner, _repoName, merge); + } + catch (Exception e) + { + Log.LogWarning($"Failed to merge the target branch into the existing pull request: {e.Message}"); + Log.LogWarning("Continuing with updating the existing pull request. You may need to resolve conflicts manually in the PR."); + } + + headSha = await GetHeadShaAsync(newBranchName); + } + + var commitSha = await CreateCommitAsync(parentTreeResponse.Sha, headSha, $"Update baselines for build {BuildLink}{buildId} (internal Microsoft link)"); + if (await ShouldMakeUpdatesAsync(headSha, commitSha)) + { + string pullRequestBody = $"This PR was created by the `CreateBaselineUpdatePR` tool for build {buildId}. \n\n" + + $"The updated test results can be found at {BuildLink}{buildId} (internal Microsoft link)"; + if (existingPullRequest != null) + { + await UpdatePullRequestAsync(newBranchName, commitSha, pullRequestBody, existingPullRequest); + } + else + { + await CreatePullRequestAsync(newBranchName, commitSha, targetBranch, title, pullRequestBody); + } + } + } + + private async Task GetExistingPullRequestAsync(string title, string targetBranch) + { + var request = new PullRequestRequest + { + Base = targetBranch + }; + var existingPullRequest = await _client.PullRequest.GetAllForRepository(_repoOwner, _repoName, request); + return existingPullRequest.FirstOrDefault(pr => pr.Title == title); + } + + private async Task CreateCommitAsync(string newSha, string headSha, string commitMessage) + { + var newCommit = new NewCommit(commitMessage, newSha, headSha); + var commit = await _client.Git.Commit.Create(_repoOwner, _repoName, newCommit); + return commit.Sha; + } + + private async Task ShouldMakeUpdatesAsync(string headSha, string commitSha) + { + var comparison = await _client.Repository.Commit.Compare(_repoOwner, _repoName, headSha, commitSha); + if (!comparison.Files.Any()) + { + Log.LogInformation("No changes to commit. Skipping PR creation/updates."); + return false; + } + return true; + } + + private async Task UpdatePullRequestAsync(string branchName, string commitSha, string body, PullRequest pullRequest) + { + await UpdateReferenceAsync(branchName, commitSha); + + var pullRequestUpdate = new PullRequestUpdate + { + Body = body + }; + await _client.PullRequest.Update(_repoOwner, _repoName, pullRequest.Number, pullRequestUpdate); + + Log.LogInformation($"Updated existing pull request #{pullRequest.Number}. URL: {pullRequest.HtmlUrl}"); + } + + private async Task CreatePullRequestAsync(string newBranchName, string commitSha, string targetBranch, string title, string body) + { + await CreateReferenceAsync(newBranchName, commitSha); + + var newPullRequest = new NewPullRequest(title, newBranchName, targetBranch) + { + Body = body + }; + var pullRequest = await _client.PullRequest.Create(_repoOwner, _repoName, newPullRequest); + + Log.LogInformation($"Created pull request #{pullRequest.Number}. URL: {pullRequest.HtmlUrl}"); + } + + private async Task GetHeadShaAsync(string branchName) + { + var reference = await _client.Git.Reference.Get(_repoOwner, _repoName, $"heads/{branchName}"); + return reference.Object.Sha; + } + + private async Task UpdateReferenceAsync(string branchName, string commitSha) + { + var referenceUpdate = new ReferenceUpdate(commitSha); + await _client.Git.Reference.Update(_repoOwner, _repoName, $"heads/{branchName}", referenceUpdate); + } + + private async Task CreateReferenceAsync(string branchName, string commitSha) + { + var newReference = new NewReference($"refs/heads/{branchName}", commitSha); + await _client.Git.Reference.Create(_repoOwner, _repoName, newReference); + } +} diff --git a/src/SourceBuild/content/eng/tools/CreateBaselineUpdatePR/Pipelines.cs b/src/SourceBuild/content/eng/tools/CreateBaselineUpdatePR/Pipelines.cs new file mode 100644 index 000000000..9b4a9e925 --- /dev/null +++ b/src/SourceBuild/content/eng/tools/CreateBaselineUpdatePR/Pipelines.cs @@ -0,0 +1,10 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace CreateBaselineUpdatePR; + +public enum Pipelines +{ + Sdk, + License +} diff --git a/src/SourceBuild/content/eng/tools/CreateBaselineUpdatePR/Program.cs b/src/SourceBuild/content/eng/tools/CreateBaselineUpdatePR/Program.cs new file mode 100644 index 000000000..274a06004 --- /dev/null +++ b/src/SourceBuild/content/eng/tools/CreateBaselineUpdatePR/Program.cs @@ -0,0 +1,118 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.CommandLine; +using Microsoft.Extensions.Logging; + +namespace CreateBaselineUpdatePR; + +public class Program +{ + public static readonly CliArgument Repo = new("repo") + { + Description = "The GitHub repository to create the PR in. Should be in the form '/'", + Arity = ArgumentArity.ExactlyOne + }; + + public static readonly CliArgument OriginalFilesDirectory = new("original-files-directory") + { + Description = "The directory where the original test files are located. Should be relative to the repo", + Arity = ArgumentArity.ExactlyOne + }; + + public static readonly CliArgument UpdatedFilesDirectory = new("updated-files-directory") + { + Description = "The directory containing the updated test files published by the associated test. Should be absolute or relative to the working directory of the tool.", + Arity = ArgumentArity.ExactlyOne + }; + + public static readonly CliArgument BuildId = new("build-id") + { + Description = "The id of the build that published the updated test files.", + Arity = ArgumentArity.ExactlyOne + }; + + public static readonly CliOption Title = new("--title", "-t") + { + Description = "The title of the PR.", + Arity = ArgumentArity.ZeroOrOne, + DefaultValueFactory = _ => "Update Test Baselines and Exclusions" + }; + + public static readonly CliOption Branch = new("--branch", "-b") + { + Description = "The target branch of the PR.", + Arity = ArgumentArity.ZeroOrOne, + DefaultValueFactory = _ => "main" + }; + + public static readonly CliOption GitHubToken = new("--github-token", "-g") + { + Description = "The GitHub token to use to create the PR.", + Arity = ArgumentArity.ZeroOrOne, + DefaultValueFactory = _ => Environment.GetEnvironmentVariable("GH_TOKEN") ?? throw new ArgumentException("GitHub token not provided.") + }; + + public static readonly CliOption Level = new("--log-level", "-l") + { + Description = "The log level to run the tool in.", + Arity = ArgumentArity.ZeroOrOne, + DefaultValueFactory = _ => LogLevel.Information, + Recursive = true + }; + + public static int ExitCode = 0; + + public static async Task Main(string[] args) + { + var sdkDiffTestsCommand = CreateCommand("sdk", "Creates a PR that updates baselines and exclusion files published by the sdk diff tests."); + var licenseScanTestsCommand = CreateCommand("license", "Creates a PR that updates baselines and exclusion files published by the license scan tests."); + + var rootCommand = new CliRootCommand("Tool for creating PRs that update baselines and exclusion files.") + { + Level, + sdkDiffTestsCommand, + licenseScanTestsCommand + }; + + SetCommandAction(sdkDiffTestsCommand, Pipelines.Sdk); + SetCommandAction(licenseScanTestsCommand, Pipelines.License); + + await rootCommand.Parse(args).InvokeAsync(); + + return ExitCode; + } + + private static CliCommand CreateCommand(string name, string description) + { + return new CliCommand(name, description) + { + Repo, + OriginalFilesDirectory, + UpdatedFilesDirectory, + BuildId, + Title, + Branch, + GitHubToken + }; + } + + private static void SetCommandAction(CliCommand command, Pipelines pipeline) + { + command.SetAction(async (result, CancellationToken) => + { + Log.Level = result.GetValue(Level); + + var creator = new PRCreator(result.GetValue(Repo)!, result.GetValue(GitHubToken)!); + + ExitCode = await creator.ExecuteAsync( + result.GetValue(OriginalFilesDirectory)!, + result.GetValue(UpdatedFilesDirectory)!, + result.GetValue(BuildId)!, + result.GetValue(Title)!, + result.GetValue(Branch)!, + pipeline); + }); + } +} diff --git a/src/SourceBuild/content/test/Microsoft.DotNet.SourceBuild.SmokeTests/LicenseScanTests.cs b/src/SourceBuild/content/test/Microsoft.DotNet.SourceBuild.SmokeTests/LicenseScanTests.cs index d6b4ba920..852507964 100644 --- a/src/SourceBuild/content/test/Microsoft.DotNet.SourceBuild.SmokeTests/LicenseScanTests.cs +++ b/src/SourceBuild/content/test/Microsoft.DotNet.SourceBuild.SmokeTests/LicenseScanTests.cs @@ -144,13 +144,16 @@ public class LicenseScanTests : TestBase { Assert.NotNull(Config.LicenseScanPath); + // Indicates how long until a timeout occurs for scanning a given file + const int FileScanTimeoutSeconds = 240; + string scancodeResultsPath = Path.Combine(LogsDirectory, "scancode-results.json"); // Scancode Doc: https://scancode-toolkit.readthedocs.io/en/latest/index.html string ignoreOptions = string.Join(" ", s_ignoredFilePatterns.Select(pattern => $"--ignore {pattern}")); ExecuteHelper.ExecuteProcessValidateExitCode( "scancode", - $"--license --processes 4 --strip-root --only-findings {ignoreOptions} --json-pp {scancodeResultsPath} {Config.LicenseScanPath}", + $"--license --processes 4 --timeout {FileScanTimeoutSeconds} --strip-root --only-findings {ignoreOptions} --json-pp {scancodeResultsPath} {Config.LicenseScanPath}", OutputHelper); JsonDocument doc = JsonDocument.Parse(File.ReadAllText(scancodeResultsPath));