diff --git a/IssueCloser/IssueCloser.csproj b/IssueCloser/IssueCloser.csproj index 50d66b92..c7fe5684 100644 --- a/IssueCloser/IssueCloser.csproj +++ b/IssueCloser/IssueCloser.csproj @@ -16,7 +16,7 @@ - + diff --git a/IssueCloser/Program.cs b/IssueCloser/Program.cs index b81b3f5e..ffd1cd5f 100644 --- a/IssueCloser/Program.cs +++ b/IssueCloser/Program.cs @@ -1,177 +1,194 @@ -using DotNetDocs.Tools.Utility; -using DotNetDocs.Tools.GitHubCommunications; -using DotNetDocs.Tools.GraphQLQueries; -using Microsoft.DotnetOrg.Ospo; +using System.CommandLine; using System.Text.Json; using DotNet.DocsTools.GitHubObjects; using DotNet.DocsTools.GraphQLQueries; +using DotNetDocs.Tools.GitHubCommunications; +using DotNetDocs.Tools.GraphQLQueries; +using DotNetDocs.Tools.Utility; +using Microsoft.DotnetOrg.Ospo; +using IssueCloser; +using System.CommandLine.Parsing; -namespace IssueCloser; - -class Program -{ - private const string ConfigFile = "bulkcloseconfig.json"; - // shoag, rpetrusha worked with us, but have retired: - private static readonly string[] teamAuthors = ["shoag", "rpetrusha"]; +const string ConfigFile = "bulkcloseconfig.json"; - // The text of the comment to add: - private const string commentText = +// The text of the comment to add: +const string commentText = @"This issue has been closed as part of the issue backlog grooming process outlined in #22351. That automated process may have closed some issues that should be addressed. If you think this is one of them, reopen it with a comment explaining why. Tag the `@dotnet/docs` team for visibility."; - /// - /// Close issues based on age, author (customer or MS employee) and priority labels. - /// This was written for (hopefully) a one-time situation. The dotnet/docs repo - /// reached over 1500 issues. We couldn't plan effectively, and we bulk closed - /// a number of issues - /// The tool search issues for candidates based on age, if the author is a customer - /// or MS employee, and priority labels. Issues that are over the threshold are closed. - /// In addition, those issues have a comment pointing to a master issue that describes - /// the process. - /// - /// The organization managing the repo. - /// The repository to update. - /// True to list work, but not actually close any issue. - /// 0 on success, a non-zero number on error conditions. - static async Task Main(string organization = "dotnet", string repository = "docs", bool dryRun = false) - { - var key = CommandLineUtility.GetEnvVariable("GitHubBotKey", - "You must store the bot's GitHub key in the 'GitHubBotKey' environment variable", - ""); - var ospoKey = CommandLineUtility.GetEnvVariable("OspoKey", - "You must store your OSPO key in the 'OspoKey' environment variable", - ""); +var (organization, repository, dryRun) = ParseArguments(args); - var client = IGitHubClient.CreateGitHubClient(key); - var ospoClient = new OspoClient(ospoKey, true); +var key = CommandLineUtility.GetEnvVariable("GitHubBotKey", + "You must store the bot's GitHub key in the 'GitHubBotKey' environment variable", + ""); +var ospoKey = CommandLineUtility.GetEnvVariable("OspoKey", + "You must store your OSPO key in the 'OspoKey' environment variable", + ""); - var labelQuery = new ScalarQuery(client); +var client = IGitHubClient.CreateGitHubClient(key); +var ospoClient = new OspoClient(ospoKey, true); - var label = await labelQuery.PerformQuery(new FindLabelQueryVariables(organization, repository, "won't fix")); - if (label is null) - { - Console.WriteLine($"Could not find label [won't fix]"); - return -1; - } - var labelID = label.Id; +var labelQuery = new ScalarQuery(client); - try - { - // Next, starting paging through all issues: - Console.WriteLine("Processing open issues"); - await ProcessIssues(client, ospoClient, organization, repository, dryRun, labelID); - return 0; - } - catch (InvalidOperationException e) - { - Console.WriteLine(e.Message); - return -1; - } - } +var label = await labelQuery.PerformQuery(new FindLabelQueryVariables(organization, repository, "won't fix")); +if (label is null) +{ + Console.WriteLine($"Could not find label [won't fix]"); + return -1; +} +var labelID = label.Id; - private static async Task ProcessIssues(IGitHubClient client, OspoClient ospoClient, string organization, string repository, bool dryRun, string labelID) - { - var query = new EnumerationQuery(client); - var now = DateTime.Now; +try +{ + // Next, starting paging through all issues: + Console.WriteLine("Processing open issues"); + await ProcessIssues(client, ospoClient, organization, repository, dryRun, labelID); + return 0; +} +catch (InvalidOperationException e) +{ + Console.WriteLine(e.Message); + return -1; +} - var stats = await BuildStatsMapAsync(); +static async Task ProcessIssues(IGitHubClient client, OspoClient ospoClient, string organization, string repository, bool dryRun, string labelID) +{ + var query = new EnumerationQuery(client); + var now = DateTime.Now; + + var stats = await BuildStatsMapAsync(); - int totalClosedIssues = 0; - int totalIssues = 0; - await foreach (var item in query.PerformQuery(new BankruptcyIssueVariables(organization, repository))) + int totalClosedIssues = 0; + int totalIssues = 0; + await foreach (var item in query.PerformQuery(new BankruptcyIssueVariables(organization, repository))) + { + var issueID = item.Id; + + var priority = Priorities.PriLabel(item.Labels); + bool isInternal = await item.Author.IsMicrosoftFTE(ospoClient) == true; + bool isDocIssue = IsDocsIssue(item.Body); + int ageInMonths = (int)(now - item.CreatedDate).TotalDays / 30; + var criteria = new CloseCriteria(priority, isDocIssue, isInternal); + var number = item.Number; + var title = item.Title; + + totalIssues++; + if (stats[criteria].ShouldCloseIssue(criteria, ageInMonths)) { - var issueID = item.Id; - - var priority = Priorities.PriLabel(item.Labels); - bool isInternal = await item.Author.IsMicrosoftFTE(ospoClient) == true; - if (teamAuthors.Contains(item.Author?.Login)) - isInternal = true; - bool isDocIssue = IsDocsIssue(item.Body); - int ageInMonths = (int)(now - item.CreatedDate).TotalDays / 30; - var criteria = new CloseCriteria(priority, isDocIssue, isInternal); - var number = item.Number; - var title = item.Title; - - totalIssues++; - if (stats[criteria].ShouldCloseIssue(criteria, ageInMonths)) - { - Console.WriteLine($"Recommend Closing [{number} - {title}]"); - Console.WriteLine($"\t{criteria}, {ageInMonths}"); + Console.WriteLine($"Recommend Closing [{number} - {title}]"); + Console.WriteLine($"\t{criteria}, {ageInMonths}"); - totalClosedIssues++; - if (!dryRun) - { - await CloseIssue(client, issueID, labelID); - Console.WriteLine($"!!!!! Issue CLOSED {number}-{title} !!!!!"); - } + totalClosedIssues++; + if (!dryRun) + { + await CloseIssue(client, issueID, labelID); + Console.WriteLine($"!!!!! Issue CLOSED {number}-{title} !!!!!"); } } - - foreach (var item in stats.Where(item => item.Value.TotalIssues > 0)) - { - Console.WriteLine($"- {item.Key}:\n - {item.Value}"); - } - - Console.WriteLine($"Closing {totalClosedIssues} of {totalIssues}"); } - private static async Task CloseIssue(IGitHubClient client, string issueID, string labelID) + foreach (var item in stats.Where(item => item.Value.TotalIssues > 0)) { - // 1. Add label - Console.WriteLine($"\tAdding [won't fix] label."); - Console.WriteLine($"\tAdding Closing comment."); - Console.WriteLine($"\tClosing issue."); - var closeIssueMutation = new Mutation(client); - await closeIssueMutation.PerformMutation(new CloseIssueVariables(issueID, labelID, commentText)); + Console.WriteLine($"- {item.Key}:\n - {item.Value}"); } - private static async Task> BuildStatsMapAsync() - { - using FileStream openStream = File.OpenRead(ConfigFile); - List? items = await JsonSerializer.DeserializeAsync>(openStream); - // Uncomment this to build the config file for the first time: - Dictionary map = new Dictionary(); + Console.WriteLine($"Closing {totalClosedIssues} of {totalIssues}"); +} - if (items != null) +static async Task CloseIssue(IGitHubClient client, string issueID, string labelID) +{ + // 1. Add label + Console.WriteLine($"\tAdding [won't fix] label."); + Console.WriteLine($"\tAdding Closing comment."); + Console.WriteLine($"\tClosing issue."); + var closeIssueMutation = new Mutation(client); + await closeIssueMutation.PerformMutation(new CloseIssueVariables(issueID, labelID, commentText)); +} + +static async Task> BuildStatsMapAsync() +{ + using FileStream openStream = File.OpenRead(ConfigFile); + List? items = await JsonSerializer.DeserializeAsync>(openStream); + // Uncomment this to build the config file for the first time: + Dictionary map = new Dictionary(); + + if (items != null) + { + foreach (var configItem in items) { - foreach (var configItem in items) - { - map.Add(configItem.Criteria, new IssueSet { AgeToClose = configItem.AgeInMonths }); - } + map.Add(configItem.Criteria, new IssueSet { AgeToClose = configItem.AgeInMonths }); } - else + } + else + { + items = new List(); + int ageIndex = 0; + foreach (var botPriority in System.Enum.GetValues()) { - items = new List(); - int ageIndex = 0; - foreach (var botPriority in System.Enum.GetValues()) + for (int codeBlock = 0; codeBlock < 2; codeBlock++) { - for (int codeBlock = 0; codeBlock < 2; codeBlock++) + for (int author = 0; author < 2; author++) { - for (int author = 0; author < 2; author++) - { - var criteria = new CloseCriteria(botPriority, codeBlock == 0, author == 0); - int age = IssueSet.Ages[ageIndex++]; - items.Add(new(criteria, age)); - map.Add(criteria, - new IssueSet { AgeToClose = age }); - } + var criteria = new CloseCriteria(botPriority, codeBlock == 0, author == 0); + int age = IssueSet.Ages[ageIndex++]; + items.Add(new(criteria, age)); + map.Add(criteria, + new IssueSet { AgeToClose = age }); } } - - using FileStream createStream = File.Create(ConfigFile); - await JsonSerializer.SerializeAsync(createStream, items, new JsonSerializerOptions() { WriteIndented = true }); } - return map; + + using FileStream createStream = File.Create(ConfigFile); + await JsonSerializer.SerializeAsync(createStream, items, new JsonSerializerOptions() { WriteIndented = true }); } - private static bool IsDocsIssue(string? body) + return map; +} +static bool IsDocsIssue(string? body) +{ + const string header1 = "---"; + const string header2 = "#### "; + const string header3 = "⚠ *"; + + return (body != null) && body.Contains(header1) && + body.Contains(header2) && + body.Contains(header3); +} + +static (string organization, string repository, bool dryRun) ParseArguments(string[] args) +{ + Option organizationOption = new("--organization") + { + Description = "The GitHub organization for the target repository.", + DefaultValueFactory = parseResult => "dotnet" + }; + Option repositoryOption = new("--repository") { - const string header1 = "---"; - const string header2 = "#### "; - const string header3 = "⚠ *"; + Description = "The GitHub target repository.", + DefaultValueFactory = parseResult => "docs" + }; + Option dryRunOption = new("--dryRun") + { + Description = "Flag to specify a dry run (no issues will be closed).", + DefaultValueFactory = parseResult => false + }; + RootCommand rootCommand = new("Issue Closer application."); + + rootCommand.Options.Add(organizationOption); + rootCommand.Options.Add(repositoryOption); + rootCommand.Options.Add(dryRunOption); - return (body != null) && body.Contains(header1) && - body.Contains(header2) && - body.Contains(header3); + ParseResult result = rootCommand.Parse(args); + foreach (ParseError parseError in result.Errors) + { + Console.Error.WriteLine(parseError.Message); + } + if (result.Errors.Count > 0) + { + throw new InvalidOperationException("Invalid command line."); } + var organization = result.GetValue(organizationOption) ?? throw new InvalidOperationException("organization is null"); + var repository = result.GetValue(repositoryOption) ?? throw new InvalidOperationException("repository is null"); + var dryRun = result.GetValue(dryRunOption); + return (organization, repository, dryRun); } diff --git a/actions/sequester/ImportIssues/ImportIssues.csproj b/actions/sequester/ImportIssues/ImportIssues.csproj index ead02c11..d0f3eb3e 100644 --- a/actions/sequester/ImportIssues/ImportIssues.csproj +++ b/actions/sequester/ImportIssues/ImportIssues.csproj @@ -1,4 +1,4 @@ - + Exe net10.0 @@ -15,8 +15,7 @@ - - + diff --git a/actions/sequester/ImportIssues/Program.cs b/actions/sequester/ImportIssues/Program.cs index 252ba04b..2a582934 100644 --- a/actions/sequester/ImportIssues/Program.cs +++ b/actions/sequester/ImportIssues/Program.cs @@ -1,116 +1,150 @@ -using DotNetDocs.Tools.GitHubCommunications; +using System.CommandLine; +using System.CommandLine.Parsing; +using DotNetDocs.Tools.GitHubCommunications; using Microsoft.DotnetOrg.Ospo; -internal class Program +(string org, string repo, int? issue, int? duration, string? questConfigPath, string? branch) = ParseArguments(args); + +try { - /// - /// Process issue updates in Azure DevOps - Quest - /// - /// The GitHub organization - /// The GutHub repository names. - /// The issue number. If null, process all open issues. - /// The config path. If null, use the config file in the root folder of the repository. - /// The optional branch to use. Defaults to "main" otherwise. - /// For bulk import, how many days past to examine. -1 means all issues. Default is 5 - /// - /// Example command line: - /// ImportIssues --org dotnet --repo docs --issue 31331 - /// - /// 0 - private static async Task Main( - string org, - string repo, - int? issue = null, - int? duration = null, - string? questConfigPath = null, - string? branch = null) + if (repo.Contains('/')) + { + string[] split = repo.Split("/"); + repo = split[1]; + } + + branch ??= "main"; + Console.WriteLine($"Using branch: '{branch}'"); + + bool singleIssue = (issue is not null && issue.Value != -1); + + Console.WriteLine(singleIssue + ? $"Processing single issue {issue!.Value}: https://github.com/{org}/{repo}/issues/{issue.Value}" + : (duration is not null) && (duration != -1) + ? $"Processing all issues updated in the last {duration} days: {org}/{repo}" + : $"Processing all open issues: {org}/{repo}"); + + ImportOptions? importOptions; + if (string.IsNullOrWhiteSpace(questConfigPath) || !File.Exists(questConfigPath)) { - try - { - if (repo.Contains('/')) - { - string[] split = repo.Split("/"); - repo = split[1]; - } - - branch ??= "main"; - Console.WriteLine($"Using branch: '{branch}'"); - - bool singleIssue = (issue is not null && issue.Value != -1); - - Console.WriteLine(singleIssue - ? $"Processing single issue {issue!.Value}: https://github.com/{org}/{repo}/issues/{issue.Value}" - : (duration is not null) && (duration != -1) - ? $"Processing all issues updated in the last {duration} days: {org}/{repo}" - : $"Processing all open issues: {org}/{repo}"); - - ImportOptions? importOptions; - if (string.IsNullOrWhiteSpace(questConfigPath) || !File.Exists(questConfigPath)) - { - using RawGitHubFileReader reader = new(); - importOptions = await reader.ReadOptionsAsync(org, repo, branch); - } - else - { - LocalFileReader reader = new(); - importOptions = await reader.ReadOptionsAsync(questConfigPath); - } - - if (importOptions is null) - { - throw new ApplicationException( - $"Unable to load Quest import configuration options."); - } - - using QuestGitHubService serviceWorker = await CreateService(importOptions, !singleIssue); - - if (singleIssue) - { - await serviceWorker.ProcessIssue( - org, repo, issue!.Value); // Odd. There's a warning on issue, but it is null checked above. - } - else - { - await serviceWorker.ProcessIssues( - org, repo, duration ?? -1); - } - } - catch (InvalidOperationException e) when (e.Message.StartsWith("HTTP error:")) - { - Console.Error.WriteLine($"!!!ERROR!!! Could not communicate with Quest Azure DevOps server. Did your PAT expire?"); - Console.Error.WriteLine($":: -- {e.Message} -- "); - Console.Error.WriteLine(e.ToString()); - return 1; - } - catch (Exception ex) - { - Console.Error.WriteLine($":: -- {ex.Message} -- "); - Console.Error.WriteLine(ex.ToString()); - return 1; - } - return 0; + using RawGitHubFileReader reader = new(); + importOptions = await reader.ReadOptionsAsync(org, repo, branch); + } + else + { + LocalFileReader reader = new(); + importOptions = await reader.ReadOptionsAsync(questConfigPath); } - private static async Task CreateService(ImportOptions options, bool bulkImport) + if (importOptions is null) { - ArgumentNullException.ThrowIfNull(options.ApiKeys, nameof(options)); + throw new ApplicationException( + $"Unable to load Quest import configuration options."); + } - IGitHubClient gitHubClient = (options.ApiKeys.SequesterAppID != 0) - ? await IGitHubClient.CreateGitHubAppClient(options.ApiKeys.SequesterAppID, options.ApiKeys.SequesterPrivateKey) - : IGitHubClient.CreateGitHubClient(options.ApiKeys.GitHubToken); + using QuestGitHubService serviceWorker = await CreateService(importOptions, !singleIssue); - var ospoClient = (options.ApiKeys.AzureAccessToken is not null) - ? new OspoClient(options.ApiKeys.AzureAccessToken, false) - : null; + if (singleIssue) + { + await serviceWorker.ProcessIssue( + org, repo, issue!.Value); // Odd. There's a warning on issue, but it is null checked above. + } + else + { + await serviceWorker.ProcessIssues( + org, repo, duration ?? -1); + } +} +catch (InvalidOperationException e) when (e.Message.StartsWith("HTTP error:")) +{ + Console.Error.WriteLine($"!!!ERROR!!! Could not communicate with Quest Azure DevOps server. Did your PAT expire?"); + Console.Error.WriteLine($":: -- {e.Message} -- "); + Console.Error.WriteLine(e.ToString()); + return 1; +} +catch (Exception ex) +{ + Console.Error.WriteLine($":: -- {ex.Message} -- "); + Console.Error.WriteLine(ex.ToString()); + return 1; +} +return 0; - if (ospoClient is null) - { - Console.WriteLine("Warning: Imported work items won't be assigned based on GitHub assignee."); - } +static async Task CreateService(ImportOptions options, bool bulkImport) +{ + ArgumentNullException.ThrowIfNull(options.ApiKeys, nameof(options)); - return new QuestGitHubService( - gitHubClient, - ospoClient, - options); + IGitHubClient gitHubClient = (options.ApiKeys.SequesterAppID != 0) + ? await IGitHubClient.CreateGitHubAppClient(options.ApiKeys.SequesterAppID, options.ApiKeys.SequesterPrivateKey) + : IGitHubClient.CreateGitHubClient(options.ApiKeys.GitHubToken); + + var ospoClient = (options.ApiKeys.AzureAccessToken is not null) + ? new OspoClient(options.ApiKeys.AzureAccessToken, false) + : null; + + if (ospoClient is null) + { + Console.WriteLine("Warning: Imported work items won't be assigned based on GitHub assignee."); + } + + return new QuestGitHubService( + gitHubClient, + ospoClient, + options); +} + +static (string org, string repo, int? issue, int? duration, string? questConfigPath, string? branch) ParseArguments(string[] args) +{ + Option orgOption = new("--org") + { + Description = "The GitHub organization." + }; + Option repoOption = new("--repo") + { + Description = "The GitHub target repository.", + }; + Option issueOption = new("--issue") + { + Description = "The issue number. If null, process all changed issues.", + DefaultValueFactory = parseResult => null + }; + Option durationOption = new("--duration") + { + Description = "For bulk import, how many days past to examine. -1 means all open issues. Default is 5", + DefaultValueFactory = parseResult => null + }; + Option questConfigPathOption = new("--questConfigPath") + { + Description = "The config path. If null, use the config file in the root folder of the repository.", + }; + Option branchOption = new("--branch") + { + Description = "The optional branch to use. Defaults to \"main\" otherwise.", + DefaultValueFactory = parseResult => "main" + }; + RootCommand rootCommand = new("Sequester AzureDevOps import application"); + + rootCommand.Options.Add(orgOption); + rootCommand.Options.Add(repoOption); + rootCommand.Options.Add(issueOption); + rootCommand.Options.Add(durationOption); + rootCommand.Options.Add(questConfigPathOption); + rootCommand.Options.Add(branchOption); + + ParseResult result = rootCommand.Parse(args); + foreach (ParseError parseError in result.Errors) + { + Console.Error.WriteLine(parseError.Message); + } + if (result.Errors.Count > 0) + { + throw new InvalidOperationException("Invalid command line."); } + var org = result.GetValue(orgOption) ?? throw new InvalidOperationException("organization is null"); + var repo = result.GetValue(repoOption) ?? throw new InvalidOperationException("repository is null"); + var issue = result.GetValue(issueOption); + var duration = result.GetValue(durationOption); + var questConfigPath = result.GetValue(questConfigPathOption); + var branch = result.GetValue(branchOption); + return (org, repo, issue, duration, questConfigPath, branch); } diff --git a/snippets5000/Snippets5000/Program.cs b/snippets5000/Snippets5000/Program.cs index 7142d72d..327ed698 100644 --- a/snippets5000/Snippets5000/Program.cs +++ b/snippets5000/Snippets5000/Program.cs @@ -1,10 +1,12 @@ -using DotNetDocs.Tools.Utility; +using System.CommandLine; +using System.CommandLine.Parsing; using System.Diagnostics; -using System.Text.RegularExpressions; using System.Text.Json; +using System.Text.RegularExpressions; +using DotNet.DocsTools.Utility; +using DotNetDocs.Tools.Utility; using static Snippets5000.SnippetsConfigFile; using Log = DotNet.DocsTools.Utility.EchoLogging; -using DotNet.DocsTools.Utility; [assembly: System.Runtime.CompilerServices.InternalsVisibleTo("PullRequestSimulations")] @@ -38,15 +40,11 @@ class Program /// /// LocateProjects: Find all projects and solutions requiring a build. /// - /// The directory containing the local source tree. - /// If available, the number of the pull request being built. - /// If available, the owner organization of the repository. - /// If available, the name of the repository. - /// The test id from data.json to simulate a pull request. - /// The json file defining all the tests that can be referenced by . Usually data.json. /// 0 on success. Otherwise, a non-zero error code. - static async Task Main(string sourcepath, int? pullrequest = default, string? owner=default, string? repo=default, string? dryrunTestId=default, string? dryrunTestDataFile=default) + static async Task Main(string[] args) { + var (sourcepath, pullrequest, owner, repo, dryrunTestId, dryrunTestDataFile) = ParseArguments(args); + int exitCode = EXITCODE_GOOD; string appStartupFolder = Directory.GetCurrentDirectory(); @@ -70,7 +68,7 @@ static async Task Main(string sourcepath, int? pullrequest = default, strin // NOT a normal github PR and instead is a test else if (string.IsNullOrEmpty(dryrunTestDataFile)) - throw new ArgumentNullException(nameof(dryrunTestDataFile), "The dryrun Test DataFile must be set"); + throw new InvalidOperationException($"{nameof(dryrunTestDataFile)}: The dryrun Test DataFile must be set"); else projects = new TestingProjectList(dryrunTestId, dryrunTestDataFile, sourcepath).GenerateBuildList().ToList(); @@ -459,4 +457,62 @@ private static void ProcessFailedProjects(string repo, IEnumerable sourcePathOption = new("--sourcepath") + { + Description = "The directory containing the local source tree." + }; + Option pullrequestOption = new("--pullrequest") + { + Description = "If available, the number of the pull request being built.", + DefaultValueFactory = parseResult => null + }; + Option ownerOption = new("--owner") + { + Description = "If available, the owner organization of the repository.", + DefaultValueFactory = parseResult => null + }; + Option repoOption = new("--repo") + { + Description = "If available, the name of the repository.", + DefaultValueFactory = parseResult => null + }; + Option dryrunTestIdOption = new("--dryrun-test-id") + { + Description = "The test id from data.json to simulate a pull request.", + DefaultValueFactory = parseResult => null + }; + Option dryrunTestDataFileOption = new("--dryrun-test-data-file") + { + Description = "The json file defining all the tests that can be referenced by `dryrunTestId`. Usually data.json.", + DefaultValueFactory = parseResult => null + }; + RootCommand rootCommand = new("Snippets5000 CI build application."); + + rootCommand.Options.Add(sourcePathOption); + rootCommand.Options.Add(pullrequestOption); + rootCommand.Options.Add(ownerOption); + rootCommand.Options.Add(repoOption); + rootCommand.Options.Add(dryrunTestIdOption); + rootCommand.Options.Add(dryrunTestDataFileOption); + + ParseResult result = rootCommand.Parse(args); + foreach (ParseError parseError in result.Errors) + { + Console.Error.WriteLine(parseError.Message); + } + if (result.Errors.Count > 0) + { + throw new InvalidOperationException("Invalid command line."); + } + var sourcepath = result.GetValue(sourcePathOption) ?? throw new InvalidOperationException("organization is null"); + var pullrequest = result.GetValue(pullrequestOption); + var owner = result.GetValue(ownerOption); + var repo = result.GetValue(repoOption); + var dryrunTestId = result.GetValue(dryrunTestIdOption); + var dryrunTestDataFile = result.GetValue(dryrunTestDataFileOption); + return (sourcepath, pullrequest, owner, repo, dryrunTestId, dryrunTestDataFile); + } } diff --git a/snippets5000/Snippets5000/Snippets5000.csproj b/snippets5000/Snippets5000/Snippets5000.csproj index d7a8a9a1..f2079f50 100644 --- a/snippets5000/Snippets5000/Snippets5000.csproj +++ b/snippets5000/Snippets5000/Snippets5000.csproj @@ -11,7 +11,7 @@ $(DefineConstants);LINUX - +