Skip to content
This repository was archived by the owner on Jul 21, 2025. It is now read-only.
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,3 +18,20 @@ For more information on contributing, please visit the [contributor guide](./CON
## Contributors:

- [Akshay Hosur](https://github.com/akshay-online)

## Web Application

A new ASP.NET Core project (`ADOGenerator.Web`) exposes minimal API endpoints that leverage the existing services to create projects or generate template artifacts. This application can be deployed to Azure App Service.

The web app also serves a simple UI that lets you choose a template and create projects.

### Running locally

```bash
cd src/ADOGenerator.Web
dotnet run
```

### Deploying to Azure

Publish the project and deploy the generated output to an Azure App Service instance using the Azure portal or the `az webapp` CLI commands.
18 changes: 18 additions & 0 deletions src/ADOGenerator.Web/ADOGenerator.Web.csproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<Nullable>disable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\ADOGenerator\ADOGenerator.csproj" />
<PackageReference Include="Swashbuckle.AspNetCore" Version="6.5.0" />
</ItemGroup>

<ItemGroup>
<Content Include="..\ADOGenerator\Templates\**\*.*">
<Link>Templates\%(RecursiveDir)%(Filename)%(Extension)</Link>
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
</ItemGroup>
</Project>
26 changes: 26 additions & 0 deletions src/ADOGenerator.Web/Controllers/HomeController.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
using ADOGenerator.IServices;
using ADOGenerator.Web.ViewModels;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Rendering;
using System.Linq;

namespace ADOGenerator.Web.Controllers
{
public class HomeController : Controller
{
private readonly ITemplateService _templateService;

public HomeController(ITemplateService templateService)
{
_templateService = templateService;
}

public IActionResult Index()
{
var vm = new ProjectViewModel();
vm.TemplateOptions = _templateService.GetAvailableTemplates()
.Select(t => new SelectListItem { Text = t.Name, Value = t.TemplateFolder });
return View(vm);
}
}
}
62 changes: 62 additions & 0 deletions src/ADOGenerator.Web/Controllers/ProjectsController.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
using ADOGenerator.IServices;
using ADOGenerator.Models;
using ADOGenerator.Web.ViewModels;
using Microsoft.AspNetCore.Mvc;

namespace ADOGenerator.Web.Controllers
{
[Route("api/[controller]")]
[ApiController]
public class ProjectsController : Controller
{
private readonly IProjectService _projectService;
private readonly ITemplateService _templateService;

public ProjectsController(IProjectService projectService, ITemplateService templateService)
{
_projectService = projectService;
_templateService = templateService;
}

[HttpPost("create")]
public IActionResult Create([FromBody] ProjectViewModel request)
{
var model = new Project
{
id = Guid.NewGuid().ToString(),
accountName = request.Organization,
accessToken = request.AccessToken,
adoAuthScheme = request.AuthScheme,
ProjectName = request.ProjectName,
selectedTemplateFolder = request.TemplateFolder,
TemplateName = request.TemplateName,
isExtensionNeeded = request.InstallExtensions,
isAgreeTerms = request.InstallExtensions
};
bool created = _projectService.CreateProjectEnvironment(model);
return Ok(new { Success = created });
}

[HttpPost("artifacts")]
public IActionResult Artifacts([FromBody] ArtifactRequest request)
{
var model = new Project
{
id = Guid.NewGuid().ToString(),
accountName = request.Organization,
ProjectName = request.ProjectName,
ProjectId = request.ProjectId,
accessToken = request.AccessToken,
adoAuthScheme = request.AuthScheme
};
var result = _templateService.GenerateTemplateArtifacts(model);
if (result.Item1)
{
return Ok(new { Template = result.Item2, Location = result.Item3 });
}
return BadRequest(new { Message = "Artifact generation failed" });
}
}

public record ArtifactRequest(string Organization, string ProjectName, string ProjectId, string AccessToken, string AuthScheme);
}
25 changes: 25 additions & 0 deletions src/ADOGenerator.Web/Controllers/TemplatesController.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
using ADOGenerator.IServices;
using Microsoft.AspNetCore.Mvc;
using System.Linq;

namespace ADOGenerator.Web.Controllers
{
[Route("api/[controller]")]
[ApiController]
public class TemplatesController : Controller
{
private readonly ITemplateService _templateService;
public TemplatesController(ITemplateService templateService)
{
_templateService = templateService;
}

[HttpGet]
public IActionResult Get()
{
var list = _templateService.GetAvailableTemplates()
.Select(t => new { t.Name, t.TemplateFolder, t.Description });
return Ok(list);
}
}
}
35 changes: 35 additions & 0 deletions src/ADOGenerator.Web/Program.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
using ADOGenerator.IServices;
using ADOGenerator.Models;
using ADOGenerator.Services;

var builder = WebApplication.CreateBuilder(args);

// Add configuration and services
builder.Configuration.AddJsonFile("appsettings.json", optional: true, reloadOnChange: true);

builder.Services.AddSingleton<IProjectService, ProjectService>();
builder.Services.AddSingleton<ITemplateService, TemplateService>();
builder.Services.AddSingleton<IAuthService, AuthService>();

builder.Services.AddControllersWithViews();
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();

var app = builder.Build();

app.UseStaticFiles();
app.UseRouting();

if (app.Environment.IsDevelopment())
{
app.UseSwagger();
app.UseSwaggerUI();
}

app.UseAuthorization();

app.MapControllerRoute(
name: "default",
pattern: "{controller=Home}/{action=Index}/{id?}");

app.Run();
18 changes: 18 additions & 0 deletions src/ADOGenerator.Web/ViewModels/ProjectViewModel.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
using Microsoft.AspNetCore.Mvc.Rendering;
using System.Collections.Generic;

namespace ADOGenerator.Web.ViewModels
{
public class ProjectViewModel
{
public string Organization { get; set; }
public string ProjectName { get; set; }
public string TemplateName { get; set; }
public string TemplateFolder { get; set; }
public string AccessToken { get; set; }
public string AuthScheme { get; set; } = "pat";
public bool InstallExtensions { get; set; } = true;
public IEnumerable<SelectListItem> TemplateOptions { get; set; } = new List<SelectListItem>();
public string ResultMessage { get; set; }
}
}
42 changes: 42 additions & 0 deletions src/ADOGenerator.Web/Views/Home/Index.cshtml
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
@model ADOGenerator.Web.ViewModels.ProjectViewModel
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<title>ADO Generator</title>
<style>
body { font-family: Arial, sans-serif; margin: 2rem; }
label { display:block; margin-top:1rem; }
select, input[type=text], input[type=password] { width: 300px; }
button { margin-top:1rem; padding:0.5rem 1rem; }
</style>
</head>
<body>
<h1>Create Azure DevOps Project</h1>
<form asp-action="Create" asp-controller="Projects" method="post">
<label>Organization
<input asp-for="Organization" />
</label>
<label>Project Name
<input asp-for="ProjectName" />
</label>
<label>Template
<select asp-for="TemplateFolder" asp-items="Model.TemplateOptions"></select>
</label>
<label>Access Token
<input asp-for="AccessToken" type="password" />
</label>
<label>Auth Scheme
<input asp-for="AuthScheme" />
</label>
<label>
<input asp-for="InstallExtensions" type="checkbox" /> Install Extensions
</label>
<button type="submit">Create Project</button>
</form>
@if (Model.ResultMessage != null)
{
<pre>@Model.ResultMessage</pre>
}
</body>
</html>
1 change: 1 addition & 0 deletions src/ADOGenerator.Web/Views/_ViewImports.cshtml
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers
3 changes: 3 additions & 0 deletions src/ADOGenerator.Web/Views/_ViewStart.cshtml
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
@{
Layout = null;
}
46 changes: 46 additions & 0 deletions src/ADOGenerator.Web/appsettings.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
{
"AppSettings": {
"webpages:Version": "2.0.0.0",
"webpages:Enabled": "false",
"PreserveLoginUrl": "true",
"ClientValidationEnabled": "true",
"UnobtrusiveJavaScriptEnabled": "true",
"ProjectCreationVersion": "4.1",
"ProjectPropertyVersion": "4.1-preview",
"RepoVersion": "4.1",
"BuildVersion": "4.1",
"ReleaseVersion": "4.1",
"WikiVersion": "4.1",
"BoardVersion": "4.1",
"WorkItemsVersion": "4.1",
"QueriesVersion": "4.1",
"EndPointVersion": "4.1-preview.1",
"ExtensionVersion": "4.1",
"DashboardVersion": "4.1-preview.2",
"AgentQueueVersion": "4.1-preview",
"GetSourceCodeVersion": "4.1-preview",
"TestPlanVersion": "5.0",
"DefaultHost": "https://dev.azure.com/",
"ReleaseHost": "https://vsrm.dev.azure.com/",
"ExtensionHost": "https://extmgmt.dev.azure.com/",
"GetRelease": "4.1-preview.3",
"BaseAddress": "https://app.vssps.visualstudio.com/",
"GraphAPIHost": "https://vssps.dev.azure.com/",
"ProjectProperties": "4.1-preview.1",
"DefaultTemplate": "SmartHotel360",
"DeloymentGroup": "4.1-preview.1",
"GraphApiVersion": "4.1-preview.1",
"VariableGroupsApiVersion": "5.0-preview.1",
"AnalyticsKey": "",
"clientId": "",
"tenantId": "",
"scopes": ""
},
"Logging": {
"LogLevel": {
"Default": "Information",
"System": "Warning",
"Microsoft": "Warning"
}
}
}
1 change: 1 addition & 0 deletions src/ADOGenerator/IServices/ITemplateService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,5 +7,6 @@ public interface ITemplateService
bool AnalyzeProject(Project model);
bool CheckTemplateExists(Project model);
(bool,string,string) GenerateTemplateArtifacts(Project model);
IEnumerable<TemplateSelection.Template> GetAvailableTemplates();
}
}
10 changes: 5 additions & 5 deletions src/ADOGenerator/Services/ProjectService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -183,7 +183,7 @@
public string GetJsonFilePath(bool IsPrivate, string TemplateFolder, string TemplateName, string FileName = "")
{
string filePath = string.Empty;
filePath = string.Format(Path.Combine(Directory.GetCurrentDirectory(), "Templates", TemplateName, FileName));
filePath = Path.Combine(AppContext.BaseDirectory, "Templates", TemplateName, FileName);
return filePath;
}

Expand Down Expand Up @@ -297,9 +297,9 @@

string projTemplateFile = GetJsonFilePath(model.IsPrivatePath, model.PrivateTemplatePath, templateUsed, "ProjectTemplate.json");
ProjectSetting setting = null;
if (File.Exists(projTemplateFile))

Check failure on line 300 in src/ADOGenerator/Services/ProjectService.cs

View check run for this annotation

GitHub Advanced Security / CodeQL

Uncontrolled data used in path expression

This path depends on a [user-provided value](1).
{
string _checkIsPrivate = File.ReadAllText(projTemplateFile);

Check failure on line 302 in src/ADOGenerator/Services/ProjectService.cs

View check run for this annotation

GitHub Advanced Security / CodeQL

Uncontrolled data used in path expression

This path depends on a [user-provided value](1).
if (_checkIsPrivate != "")
{
setting = JsonConvert.DeserializeObject<ProjectSetting>(_checkIsPrivate);
Expand All @@ -309,7 +309,7 @@
string projectSettingsFile = string.Empty;
try
{
if (File.Exists(projTemplateFile))

Check failure on line 312 in src/ADOGenerator/Services/ProjectService.cs

View check run for this annotation

GitHub Advanced Security / CodeQL

Uncontrolled data used in path expression

This path depends on a [user-provided value](1).
{
string templateItems = model.ReadJsonFile(projTemplateFile);
template = JsonConvert.DeserializeObject<ProjectTemplate>(templateItems);
Expand Down Expand Up @@ -366,7 +366,7 @@
Console.WriteLine("Error in reading project template file:" + ex.Message);
}
//create team project
string jsonProject = model.ReadJsonFile(Path.Combine(Directory.GetCurrentDirectory(), "Templates", "CreateProject.json"));
string jsonProject = model.ReadJsonFile(Path.Combine(AppContext.BaseDirectory, "Templates", "CreateProject.json"));
jsonProject = jsonProject.Replace("$projectName$", model.ProjectName).Replace("$processTemplateId$", processTemplateId);

Projects proj = new Projects(_projectCreationVersion);
Expand Down Expand Up @@ -2260,7 +2260,7 @@
bool isFolderCreated = false;
if (!string.IsNullOrEmpty(teamName))
{
string createQueryFolderJson = File.ReadAllText(Path.Combine(Directory.GetCurrentDirectory(), "PreSetting", "CreateQueryFolder.json"));
string createQueryFolderJson = File.ReadAllText(Path.Combine(AppContext.BaseDirectory, "PreSetting", "CreateQueryFolder.json"));
createQueryFolderJson = createQueryFolderJson.Replace("$TeamName$", teamName);
QueryResponse createFolderResponse = _newobjQuery.CreateQuery(model.ProjectName, createQueryFolderJson);
isFolderCreated = createFolderResponse.id != null ? true : false;
Expand Down Expand Up @@ -2787,7 +2787,7 @@
{
string groupDetails = "";
TemplateSelection.Templates templates = new TemplateSelection.Templates();
string templatesPath = ""; templatesPath = Path.Combine(Directory.GetCurrentDirectory(), "Templates");
string templatesPath = Path.Combine(AppContext.BaseDirectory, "Templates");
if (File.Exists(templatesPath + "TemplateSetting.json"))
{
groupDetails = File.ReadAllText(templatesPath + "\\TemplateSetting.json");
Expand Down Expand Up @@ -2889,7 +2889,7 @@
public bool WhereDoseTemplateBelongTo(string templatName)
{

string privatePath = Path.Combine(Directory.GetCurrentDirectory(), "PrivateTemplates");
string privatePath = Path.Combine(AppContext.BaseDirectory, "PrivateTemplates");
string privateTemplate = Path.Combine(privatePath, templatName);

if (!Directory.Exists(privatePath))
Expand Down
21 changes: 21 additions & 0 deletions src/ADOGenerator/Services/TemplateService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,13 @@
using ADOGenerator.Models;
using ADOGenerator.Services;
using Microsoft.Extensions.Configuration;
using Newtonsoft.Json;
using RestAPI.Extractor;
using RestAPI.ProjectsAndTeams;
using RestAPI;
using ADOGenerator;
using System.IO;
using System.Linq;

public class TemplateService : ITemplateService
{
Expand Down Expand Up @@ -101,6 +104,24 @@ public bool CheckTemplateExists(Project model)
}
}

public IEnumerable<TemplateSelection.Template> GetAvailableTemplates()
{
var templatesPath = Path.Combine(AppContext.BaseDirectory, "Templates", "TemplateSetting.json");
if (!File.Exists(templatesPath))
{
return Enumerable.Empty<TemplateSelection.Template>();
}

var json = File.ReadAllText(templatesPath);
var parsed = JsonConvert.DeserializeObject<TemplateSelection.Templates>(json);
if (parsed?.GroupwiseTemplates == null)
{
return Enumerable.Empty<TemplateSelection.Template>();
}

return parsed.GroupwiseTemplates.SelectMany(g => g.Template);
}

private void LogAnalysisResults(Project model, ExtractorAnalysis analysis)
{
model.id.AddMessage(Environment.NewLine + "-------------------------------------------------------------------");
Expand Down
Loading