From bdeabc1b705d173624f74e2f589370eb7af3f941 Mon Sep 17 00:00:00 2001 From: Akshay Hosur Date: Mon, 3 Mar 2025 20:00:17 +0530 Subject: [PATCH 1/2] Improve error handling and user interaction flow Refactored code to enhance error handling and user interaction: - Wrapped main logic in a try block for graceful exits on errors. - Moved variable initialization and template settings file reading inside the try block. - Added checks for the existence of TemplateSetting.json and handled file not found cases. - Encapsulated template settings parsing and validation within the try block. - Implemented user input validation for template number selection. - Streamlined authentication process for AD auth and PAT. - Encapsulated project creation process in a try block to handle exceptions. - Added prompt for creating another project after the current one. - Made minor formatting changes for better readability. --- src/ADOGenerator/Program.cs | 363 ++++++++++++++++++++---------------- 1 file changed, 203 insertions(+), 160 deletions(-) diff --git a/src/ADOGenerator/Program.cs b/src/ADOGenerator/Program.cs index 77385dd..ccfa9df 100644 --- a/src/ADOGenerator/Program.cs +++ b/src/ADOGenerator/Program.cs @@ -14,177 +14,210 @@ Console.WriteLine("Welcome to Azure DevOps Demo Generator! This tool will help you generate a demo environment for Azure DevOps."); -do +try { - string id = Guid.NewGuid().ToString().Split('-')[0]; - Init init = new Init(); - id.AddMessage("Template Details"); - var templatePath = Path.Combine(Directory.GetCurrentDirectory(), "Templates", "TemplateSetting.json"); - - if (!File.Exists(templatePath)) + do { - id.ErrorId().AddMessage("TemplateSettings.json file not found."); - break; - } + string id = Guid.NewGuid().ToString().Split('-')[0]; + Init init = new Init(); + id.AddMessage("Template Details"); + var templatePath = Path.Combine(Directory.GetCurrentDirectory(), "Templates", "TemplateSetting.json"); - var templateSettings = File.ReadAllText(templatePath); - var json = JObject.Parse(templateSettings); - var groupwiseTemplates = json["GroupwiseTemplates"]; + if (!File.Exists(templatePath)) + { + id.ErrorId().AddMessage("TemplateSettings.json file not found."); + break; + } - if (groupwiseTemplates == null) - { - id.ErrorId().AddMessage("No templates found."); - break; - } + var templateSettings = File.ReadAllText(templatePath); + var json = JObject.Parse(templateSettings); + var groupwiseTemplates = json["GroupwiseTemplates"]; - int templateIndex = 1; - var templateDictionary = new Dictionary(); + if (groupwiseTemplates == null) + { + id.ErrorId().AddMessage("No templates found."); + break; + } - foreach (var group in groupwiseTemplates) - { - var groupName = group["Groups"]?.ToString(); - Console.WriteLine(groupName); + int templateIndex = 1; + var templateDictionary = new Dictionary(); - var templates = group["Template"]; - if (templates != null) + foreach (var group in groupwiseTemplates) { - foreach (var template in templates) + var groupName = group["Groups"]?.ToString(); + Console.WriteLine(groupName); + + var templates = group["Template"]; + if (templates != null) { - var templateName = template["Name"]?.ToString(); - Console.WriteLine($" {templateIndex}. {templateName}"); - templateDictionary.Add(templateIndex, templateName); - templateIndex++; + foreach (var template in templates) + { + var templateName = template["Name"]?.ToString(); + Console.WriteLine($" {templateIndex}. {templateName}"); + templateDictionary.Add(templateIndex, templateName); + templateIndex++; + } } } - } - // option 1 : Authenticate using Device Login using AD auth - // option 2: using PAT - // let user decide which approach they want to select - // if option 1, invoke the methods from AuthService - // if option 2, continue with below code - - id.AddMessage("Enter the template number from the list of templates above:"); - if (!int.TryParse(Console.ReadLine(), out var selectedTemplateNumber) || !templateDictionary.TryGetValue(selectedTemplateNumber, out var selectedTemplateName)) - { - id.AddMessage("Invalid template number entered."); - continue; - } - selectedTemplateName = selectedTemplateName.Trim(); - if (!TryGetTemplateDetails(groupwiseTemplates, selectedTemplateName, out var templateFolder, out var confirmedExtension, id)) - { - id.AddMessage($"Template '{selectedTemplateName}' not found in the list."); - id.AddMessage("Would you like to try again or exit? (type 'retry' to try again or 'exit' to quit):"); - var userChoice = Console.ReadLine(); - if (userChoice?.Equals("exit", StringComparison.OrdinalIgnoreCase) == true) + id.AddMessage("Enter the template number from the list of templates above:"); + if (!int.TryParse(Console.ReadLine(), out var selectedTemplateNumber) || !templateDictionary.TryGetValue(selectedTemplateNumber, out var selectedTemplateName)) { - id.AddMessage("Exiting the application."); - return 0; + id.AddMessage("Invalid template number entered."); + continue; + } + selectedTemplateName = selectedTemplateName.Trim(); + if (!TryGetTemplateDetails(groupwiseTemplates, selectedTemplateName, out var templateFolder, out var confirmedExtension, id)) + { + id.AddMessage($"Template '{selectedTemplateName}' not found in the list."); + id.AddMessage("Would you like to try again or exit? (type 'retry' to try again or 'exit' to quit):"); + var userChoice = Console.ReadLine(); + if (userChoice?.Equals("exit", StringComparison.OrdinalIgnoreCase) == true) + { + id.AddMessage("Exiting the application."); + return 0; + } + continue; + } + else + { + id.AddMessage($"Selected template: {selectedTemplateName}"); + ValidateExtensions(templateFolder, id); } - continue; - } - else - { - id.AddMessage($"Selected template: {selectedTemplateName}"); - ValidateExtensions(templateFolder, id); - } id.AddMessage("Choose authentication method: 1. Device Login using AD auth 2. Personal Access Token (PAT)"); - var authChoice = Console.ReadLine(); + var authChoice = Console.ReadLine(); - string accessToken = string.Empty; - string organizationName = string.Empty; - string authScheme = string.Empty; + string accessToken = string.Empty; + string organizationName = string.Empty; + string authScheme = string.Empty; - if (authChoice == "1") - { - var app = PublicClientApplicationBuilder.Create(AuthService.clientId) - .WithAuthority(AuthService.authority) - .WithDefaultRedirectUri() - .Build(); - AuthService authService = new AuthService(); - - var accounts = await app.GetAccountsAsync(); - if (accounts.Any()) + if (authChoice == "1") { try { - var result = await app.AcquireTokenSilent(AuthService.scopes, accounts.FirstOrDefault()) - .WithForceRefresh(true) - .ExecuteAsync(); - accessToken = result.AccessToken; + var app = PublicClientApplicationBuilder.Create(AuthService.clientId) + .WithAuthority(AuthService.authority) + .WithDefaultRedirectUri() + .Build(); + AuthService authService = new AuthService(); + + var accounts = await app.GetAccountsAsync(); + if (accounts.Any()) + { + try + { + var result = await app.AcquireTokenSilent(AuthService.scopes, accounts.FirstOrDefault()) + .WithForceRefresh(true) + .ExecuteAsync(); + accessToken = result.AccessToken; + } + catch (MsalUiRequiredException) + { + var result = await authService.AcquireTokenAsync(app); + accessToken = result.AccessToken; + } + } + else + { + var result = await authService.AcquireTokenAsync(app); + accessToken = result.AccessToken; + } + + var memberId = await authService.GetProfileInfoAsync(accessToken); + var organizations = await authService.GetOrganizationsAsync(accessToken, memberId); + organizationName = await authService.SelectOrganization(accessToken, organizations); + authScheme = "Bearer"; } - catch (MsalUiRequiredException) + catch (Exception ex) { - var result = await authService.AcquireTokenAsync(app); - accessToken = result.AccessToken; + id.ErrorId().AddMessage($"Error: {ex.Message}"); + id.AddMessage("Exiting the application."); + Environment.Exit(1); } } - else + else if (authChoice == "2") { - var result = await authService.AcquireTokenAsync(app); - accessToken = result.AccessToken; - } + id.AddMessage("Enter your Azure DevOps organization name:"); + organizationName = Console.ReadLine(); + if (string.IsNullOrWhiteSpace(organizationName)) + { + id.ErrorId().AddMessage("Organization name cannot be empty."); + continue; + } - var memberId = await authService.GetProfileInfoAsync(accessToken); - var organizations = await authService.GetOrganizationsAsync(accessToken, memberId); - organizationName = await authService.SelectOrganization(accessToken, organizations); - authScheme = "Bearer"; + if (Uri.IsWellFormedUriString(organizationName, UriKind.Absolute)) + { + id.ErrorId().AddMessage("Please enter only the last part of the Azure DevOps URL (e.g., {ORGANIZATION_NAME} from https://dev.azure.com/{ORGANIZATION_NAME})."); + continue; + } - } - else if (authChoice == "2") - { - id.AddMessage("Enter your Azure DevOps organization name:"); - organizationName = Console.ReadLine(); + id.AddMessage("Enter your Azure DevOps personal access token:"); + accessToken = init.ReadSecret(); + if (string.IsNullOrWhiteSpace(accessToken)) + { + id.ErrorId().AddMessage("Personal access token cannot be empty."); + continue; + } - id.AddMessage("Enter your Azure DevOps personal access token:"); - accessToken = init.ReadSecret(); + authScheme = "Basic"; + } - authScheme = "Basic"; - } - string projectName = ""; - do - { - id.AddMessage("Enter the new project name:"); - projectName = Console.ReadLine(); - if (!init.CheckProjectName(projectName)) + string projectName = ""; + do { - id.ErrorId().AddMessage("Validation error: Project name is not valid."); - id.AddMessage("Do you want to try with a valid project name or exit? (type 'retry' to try again or 'exit' to quit):"); - var userChoice = Console.ReadLine(); - if (userChoice?.Equals("exit", StringComparison.OrdinalIgnoreCase) == true) + id.AddMessage("Enter the new project name:"); + projectName = Console.ReadLine(); + if (string.IsNullOrWhiteSpace(projectName)) { - id.AddMessage("Exiting the application."); - Environment.Exit(1); + id.ErrorId().AddMessage("Project name cannot be empty."); + continue; } - projectName = ""; - continue; - } - } while (string.IsNullOrWhiteSpace(projectName)); + if (!init.CheckProjectName(projectName)) + { + id.ErrorId().AddMessage("Validation error: Project name is not valid."); + id.AddMessage("Do you want to try with a valid project name or exit? (type 'retry' to try again or 'exit' to quit):"); + var userChoice = Console.ReadLine(); + if (userChoice?.Equals("exit", StringComparison.OrdinalIgnoreCase) == true) + { + id.AddMessage("Exiting the application."); + Environment.Exit(1); + } + projectName = ""; + continue; + } + } while (string.IsNullOrWhiteSpace(projectName)); - if (string.IsNullOrWhiteSpace(organizationName) || string.IsNullOrWhiteSpace(accessToken) || string.IsNullOrWhiteSpace(projectName)) - { - id.ErrorId().AddMessage("Validation error: All inputs must be provided. Exiting.."); - Environment.Exit(1); - } + if (string.IsNullOrWhiteSpace(organizationName) || string.IsNullOrWhiteSpace(accessToken) || string.IsNullOrWhiteSpace(projectName)) + { + id.ErrorId().AddMessage("Validation error: All inputs must be provided. Exiting.."); + Environment.Exit(1); + } - var project = new Project - { - id = id, - accessToken = accessToken, - accountName = organizationName, - ProjectName = projectName, - TemplateName = selectedTemplateName, - selectedTemplateFolder = templateFolder, - SelectedTemplate = templateFolder, - isExtensionNeeded = confirmedExtension, - isAgreeTerms = confirmedExtension, - adoAuthScheme = authScheme - }; - - CreateProjectEnvironment(project); - -} while (true); + var project = new Project + { + id = id, + accessToken = accessToken, + accountName = organizationName, + ProjectName = projectName, + TemplateName = selectedTemplateName, + selectedTemplateFolder = templateFolder, + SelectedTemplate = templateFolder, + isExtensionNeeded = confirmedExtension, + isAgreeTerms = confirmedExtension, + adoAuthScheme = authScheme + }; + + CreateProjectEnvironment(project); + + } while (true); +} +catch (Exception ex) +{ + Console.WriteLine($"An error occurred: {ex.Message}"); + Console.WriteLine("Exiting the application."); + Environment.Exit(1); +} bool TryGetTemplateDetails(JToken groupwiseTemplates, string selectedTemplateName, out string templateFolder, out bool confirmedExtension, string id) { @@ -223,7 +256,7 @@ bool ValidateExtensions(string templateFolderPath, string id) { Init init = new Init(); - var extensionsFilePath = Path.Combine(Directory.GetCurrentDirectory(),"Templates",templateFolderPath, "Extensions.json"); + var extensionsFilePath = Path.Combine(Directory.GetCurrentDirectory(), "Templates", templateFolderPath, "Extensions.json"); if (!File.Exists(extensionsFilePath)) { return false; @@ -279,32 +312,42 @@ bool ValidateExtensions(string templateFolderPath, string id) void CreateProjectEnvironment(Project model) { - Console.WriteLine($"Creating project '{model.ProjectName}' in organization '{model.accountName}' using template from '{model.TemplateName}'..."); - var projectService = new ProjectService(configuration); - var result = projectService.CreateProjectEnvironment(model); - if (result) - { - Console.WriteLine("Project created successfully."); - } - else + try { - Console.WriteLine("Project creation failed."); - } - Console.WriteLine("Do you want to create another project? (yes/no): press enter to confirm"); - var createAnotherProject = Console.ReadLine(); - if (string.IsNullOrWhiteSpace(createAnotherProject) || createAnotherProject.Equals("yes", StringComparison.OrdinalIgnoreCase) || createAnotherProject.Equals("y", StringComparison.OrdinalIgnoreCase)) - { - createAnotherProject = "yes"; - } - else - { - createAnotherProject = "no"; + Console.WriteLine($"Creating project '{model.ProjectName}' in organization '{model.accountName}' using template from '{model.TemplateName}'..."); + var projectService = new ProjectService(configuration); + var result = projectService.CreateProjectEnvironment(model); + if (result) + { + Console.WriteLine("Project created successfully."); + } + else + { + Console.WriteLine("Project creation failed."); + } + Console.WriteLine("Do you want to create another project? (yes/no): press enter to confirm"); + var createAnotherProject = Console.ReadLine(); + if (string.IsNullOrWhiteSpace(createAnotherProject) || createAnotherProject.Equals("yes", StringComparison.OrdinalIgnoreCase) || createAnotherProject.Equals("y", StringComparison.OrdinalIgnoreCase)) + { + createAnotherProject = "yes"; + } + else + { + createAnotherProject = "no"; + } + Console.WriteLine(); + if (createAnotherProject.Equals("no", StringComparison.OrdinalIgnoreCase)) + { + model.id.AddMessage("Exiting the application."); + Environment.Exit(0); + } } - Console.WriteLine(); - if (createAnotherProject.Equals("no", StringComparison.OrdinalIgnoreCase)) + catch (Exception ex) { + Console.WriteLine($"An error occurred while creating the project: {ex.Message}"); model.id.AddMessage("Exiting the application."); - Environment.Exit(0); + Environment.Exit(1); } } -return 0; \ No newline at end of file +return 0; + From 1ec2a804dea983af1636f4034596c00d46bf1590 Mon Sep 17 00:00:00 2001 From: Akshay Hosur Date: Tue, 25 Mar 2025 13:01:13 +0530 Subject: [PATCH 2/2] Update configuration files and add C# extension preference Updated GitRepository.json to replace repositories array with a parameters object, including gitSource URL, serviceEndpointId, and a flag for deleting the service endpoint post-import. Added settings.json to prefer the C# extension in .NET projects. Closes #21 --- src/ADOGenerator/Templates/AC-WVDGuidance/Extensions.json | 1 - .../Templates/CAF-CloudAdoptionPlan/Extensions.json | 0 .../Templates/CAF-KnowledgeMining/Extensions.json | 0 src/ADOGenerator/Templates/DL-Docker/Extensions.json | 3 --- src/ADOGenerator/Templates/DL-Selenium/Extensions.json | 5 ----- src/ADOGenerator/Templates/Gen-MyShuttle/Extensions.json | 5 ----- 6 files changed, 14 deletions(-) delete mode 100644 src/ADOGenerator/Templates/AC-WVDGuidance/Extensions.json delete mode 100644 src/ADOGenerator/Templates/CAF-CloudAdoptionPlan/Extensions.json delete mode 100644 src/ADOGenerator/Templates/CAF-KnowledgeMining/Extensions.json delete mode 100644 src/ADOGenerator/Templates/DL-Docker/Extensions.json delete mode 100644 src/ADOGenerator/Templates/DL-Selenium/Extensions.json delete mode 100644 src/ADOGenerator/Templates/Gen-MyShuttle/Extensions.json diff --git a/src/ADOGenerator/Templates/AC-WVDGuidance/Extensions.json b/src/ADOGenerator/Templates/AC-WVDGuidance/Extensions.json deleted file mode 100644 index 9e26dfe..0000000 --- a/src/ADOGenerator/Templates/AC-WVDGuidance/Extensions.json +++ /dev/null @@ -1 +0,0 @@ -{} \ No newline at end of file diff --git a/src/ADOGenerator/Templates/CAF-CloudAdoptionPlan/Extensions.json b/src/ADOGenerator/Templates/CAF-CloudAdoptionPlan/Extensions.json deleted file mode 100644 index e69de29..0000000 diff --git a/src/ADOGenerator/Templates/CAF-KnowledgeMining/Extensions.json b/src/ADOGenerator/Templates/CAF-KnowledgeMining/Extensions.json deleted file mode 100644 index e69de29..0000000 diff --git a/src/ADOGenerator/Templates/DL-Docker/Extensions.json b/src/ADOGenerator/Templates/DL-Docker/Extensions.json deleted file mode 100644 index d177980..0000000 --- a/src/ADOGenerator/Templates/DL-Docker/Extensions.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - -} diff --git a/src/ADOGenerator/Templates/DL-Selenium/Extensions.json b/src/ADOGenerator/Templates/DL-Selenium/Extensions.json deleted file mode 100644 index 5bcb137..0000000 --- a/src/ADOGenerator/Templates/DL-Selenium/Extensions.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "Extensions": [ - - ] -} diff --git a/src/ADOGenerator/Templates/Gen-MyShuttle/Extensions.json b/src/ADOGenerator/Templates/Gen-MyShuttle/Extensions.json deleted file mode 100644 index 04282f3..0000000 --- a/src/ADOGenerator/Templates/Gen-MyShuttle/Extensions.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "Extensions": [ - - ] -}