diff --git a/tools/identity-resolution/Helpers/GitHubToAADConverter.cs b/tools/identity-resolution/Helpers/GitHubToAADConverter.cs index e3c86eb4e8d..f3235ec6ea3 100644 --- a/tools/identity-resolution/Helpers/GitHubToAADConverter.cs +++ b/tools/identity-resolution/Helpers/GitHubToAADConverter.cs @@ -1,8 +1,11 @@ using System; +using System.Net; using System.Net.Http; +using System.Threading.Tasks; using Azure.Core; using Azure.Identity; using Microsoft.Extensions.Logging; +using Models.OpenSourcePortal; using Newtonsoft.Json; namespace Azure.Sdk.Tools.NotificationConfiguration.Helpers @@ -48,18 +51,46 @@ public GitHubToAADConverter( /// github user name /// Aad user principal name public string GetUserPrincipalNameFromGithub(string githubUserName) + { + return GetUserPrincipalNameFromGithubAsync(githubUserName).Result; + } + + public async Task GetUserPrincipalNameFromGithubAsync(string githubUserName) { try { - var responseJsonString = client.GetStringAsync($"https://repos.opensource.microsoft.com/api/people/links/github/{githubUserName}").Result; + var responseJsonString = await client.GetStringAsync($"https://repos.opensource.microsoft.com/api/people/links/github/{githubUserName}"); dynamic contentJson = JsonConvert.DeserializeObject(responseJsonString); return contentJson.aad.userPrincipalName; } + catch (HttpRequestException ex) when (ex.StatusCode == HttpStatusCode.NotFound) + { + logger.LogWarning("Github username {Username} not found", githubUserName); + } + catch (Exception ex) + { + logger.LogError(ex.Message); + } + + return null; + } + + public async Task GetPeopleLinksAsync() + { + try + { + logger.LogInformation("Calling GET https://repos.opensource.microsoft.com/api/people/links"); + var responseJsonString = await client.GetStringAsync($"https://repos.opensource.microsoft.com/api/people/links"); + var allLinks = JsonConvert.DeserializeObject(responseJsonString); + + return allLinks; + } catch (Exception ex) { logger.LogError(ex.Message); - return null; } + + return null; } } } diff --git a/tools/identity-resolution/Models/OpenSourcePortal/AadUserDetail.cs b/tools/identity-resolution/Models/OpenSourcePortal/AadUserDetail.cs new file mode 100644 index 00000000000..f43f2cf2456 --- /dev/null +++ b/tools/identity-resolution/Models/OpenSourcePortal/AadUserDetail.cs @@ -0,0 +1,15 @@ +namespace Models.OpenSourcePortal +{ + public class AadUserDetail + { + public string Alias { get; set; } + + public string PreferredName { get; set; } + + public string UserPrincipalName { get; set; } + + public string Id { get; set; } + + public string EmailAddress { get; set; } + } +} diff --git a/tools/identity-resolution/Models/OpenSourcePortal/GitHubUserDetail.cs b/tools/identity-resolution/Models/OpenSourcePortal/GitHubUserDetail.cs new file mode 100644 index 00000000000..246f88e192d --- /dev/null +++ b/tools/identity-resolution/Models/OpenSourcePortal/GitHubUserDetail.cs @@ -0,0 +1,13 @@ +namespace Models.OpenSourcePortal +{ + public class GitHubUserDetail + { + public int Id { get; set; } + + public string Login { get; set; } + + public string[] Organizations { get; set; } + + public string Avatar { get; set; } + } +} diff --git a/tools/identity-resolution/Models/OpenSourcePortal/UserLink.cs b/tools/identity-resolution/Models/OpenSourcePortal/UserLink.cs new file mode 100644 index 00000000000..26ed041bb91 --- /dev/null +++ b/tools/identity-resolution/Models/OpenSourcePortal/UserLink.cs @@ -0,0 +1,13 @@ +namespace Models.OpenSourcePortal +{ + public class UserLink + { + public GitHubUserDetail GitHub { get; set; } + + public AadUserDetail Aad { get; set; } + + public bool IsServiceAccount { get; set; } + + public string ServiceAccountContact { get; set; } + } +} diff --git a/tools/pipeline-owners-extractor/Azure.Sdk.Tools.PipelineOwnersExtractor/Azure.Sdk.Tools.PipelineOwnersExtractor.csproj b/tools/pipeline-owners-extractor/Azure.Sdk.Tools.PipelineOwnersExtractor/Azure.Sdk.Tools.PipelineOwnersExtractor.csproj new file mode 100644 index 00000000000..c2fff291567 --- /dev/null +++ b/tools/pipeline-owners-extractor/Azure.Sdk.Tools.PipelineOwnersExtractor/Azure.Sdk.Tools.PipelineOwnersExtractor.csproj @@ -0,0 +1,25 @@ + + + + Exe + net6.0 + true + pipeline-owners-extractor + + + + + + + + + + + + + + Always + + + + diff --git a/tools/pipeline-owners-extractor/Azure.Sdk.Tools.PipelineOwnersExtractor/Configuration/ISecretClientProvider.cs b/tools/pipeline-owners-extractor/Azure.Sdk.Tools.PipelineOwnersExtractor/Configuration/ISecretClientProvider.cs new file mode 100644 index 00000000000..bf56d88701c --- /dev/null +++ b/tools/pipeline-owners-extractor/Azure.Sdk.Tools.PipelineOwnersExtractor/Configuration/ISecretClientProvider.cs @@ -0,0 +1,11 @@ +using System; + +using Azure.Security.KeyVault.Secrets; + +namespace Azure.Sdk.Tools.PipelineOwnersExtractor.Configuration +{ + public interface ISecretClientProvider + { + SecretClient GetSecretClient(Uri vaultUri); + } +} \ No newline at end of file diff --git a/tools/pipeline-owners-extractor/Azure.Sdk.Tools.PipelineOwnersExtractor/Configuration/PipelineOwnerSettings.cs b/tools/pipeline-owners-extractor/Azure.Sdk.Tools.PipelineOwnersExtractor/Configuration/PipelineOwnerSettings.cs new file mode 100644 index 00000000000..2cd0d05cd5a --- /dev/null +++ b/tools/pipeline-owners-extractor/Azure.Sdk.Tools.PipelineOwnersExtractor/Configuration/PipelineOwnerSettings.cs @@ -0,0 +1,19 @@ +namespace Azure.Sdk.Tools.PipelineOwnersExtractor.Configuration +{ + public class PipelineOwnerSettings + { + public string Account { get; set; } + + public string Projects { get; set; } + + public string OpenSourceAadAppId { get; set; } + + public string OpenSourceAadSecret { get; set; } + + public string OpenSourceAadTenantId { get; set; } + + public string AzureDevOpsPat { get; set; } + + public string Output { get; set; } + } +} diff --git a/tools/pipeline-owners-extractor/Azure.Sdk.Tools.PipelineOwnersExtractor/Configuration/PostConfigureKeyVaultSettings.cs b/tools/pipeline-owners-extractor/Azure.Sdk.Tools.PipelineOwnersExtractor/Configuration/PostConfigureKeyVaultSettings.cs new file mode 100644 index 00000000000..1cb78569f71 --- /dev/null +++ b/tools/pipeline-owners-extractor/Azure.Sdk.Tools.PipelineOwnersExtractor/Configuration/PostConfigureKeyVaultSettings.cs @@ -0,0 +1,60 @@ +using System; +using System.Linq; +using System.Text.RegularExpressions; + +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; + +namespace Azure.Sdk.Tools.PipelineOwnersExtractor.Configuration +{ + public class PostConfigureKeyVaultSettings : IPostConfigureOptions where T : class + { + private static readonly Regex secretRegex = new Regex(@"(?https://.*?\.vault\.azure\.net)/secrets/(?.*)", RegexOptions.Compiled, TimeSpan.FromSeconds(5)); + private readonly ILogger logger; + private readonly ISecretClientProvider secretClientProvider; + + public PostConfigureKeyVaultSettings(ILogger> logger, ISecretClientProvider secretClientProvider) + { + this.logger = logger; + this.secretClientProvider = secretClientProvider; + } + + public void PostConfigure(string name, T options) + { + var stringProperties = typeof(T) + .GetProperties() + .Where(x => x.PropertyType == typeof(string)); + + foreach (var property in stringProperties) + { + var value = (string)property.GetValue(options); + + if (value != null) + { + var match = secretRegex.Match(value); + + if (match.Success) + { + var vaultUrl = match.Groups["vault"].Value; + var secretName = match.Groups["secret"].Value; + + try + { + var secretClient = this.secretClientProvider.GetSecretClient(new Uri(vaultUrl)); + this.logger.LogInformation("Replacing setting property {PropertyName} with value from secret {SecretUrl}", property.Name, value); + + var response = secretClient.GetSecret(secretName); + var secret = response.Value; + + property.SetValue(options, secret.Value); + } + catch (Exception exception) + { + this.logger.LogError(exception, "Unable to read secret {SecretName} from vault {VaultUrl}", secretName, vaultUrl); + } + } + } + } + } + } +} diff --git a/tools/pipeline-owners-extractor/Azure.Sdk.Tools.PipelineOwnersExtractor/Configuration/SecretClientProvider.cs b/tools/pipeline-owners-extractor/Azure.Sdk.Tools.PipelineOwnersExtractor/Configuration/SecretClientProvider.cs new file mode 100644 index 00000000000..656c457a4f3 --- /dev/null +++ b/tools/pipeline-owners-extractor/Azure.Sdk.Tools.PipelineOwnersExtractor/Configuration/SecretClientProvider.cs @@ -0,0 +1,22 @@ +using System; + +using Azure.Core; +using Azure.Security.KeyVault.Secrets; + +namespace Azure.Sdk.Tools.PipelineOwnersExtractor.Configuration +{ + public class SecretClientProvider : ISecretClientProvider + { + private readonly TokenCredential tokenCredential; + + public SecretClientProvider(TokenCredential tokenCredential) + { + this.tokenCredential = tokenCredential; + } + + public SecretClient GetSecretClient(Uri vaultUri) + { + return new SecretClient(vaultUri, this.tokenCredential); + } + } +} \ No newline at end of file diff --git a/tools/pipeline-owners-extractor/Azure.Sdk.Tools.PipelineOwnersExtractor/Extensions.cs b/tools/pipeline-owners-extractor/Azure.Sdk.Tools.PipelineOwnersExtractor/Extensions.cs new file mode 100644 index 00000000000..67f2e56a5e2 --- /dev/null +++ b/tools/pipeline-owners-extractor/Azure.Sdk.Tools.PipelineOwnersExtractor/Extensions.cs @@ -0,0 +1,48 @@ +using System.Collections.Generic; +using System.Threading.Tasks; + +namespace Azure.Sdk.Tools.PipelineOwnersExtractor +{ + public static class Extensions + { + public static async Task LimitConcurrencyAsync(this IEnumerable> tasks, int concurrencyLimit = 1) + { + if (concurrencyLimit == int.MaxValue) + { + return await Task.WhenAll(tasks); + } + + var results = new List(); + + if (concurrencyLimit == 1) + { + foreach (var task in tasks) + { + results.Add(await task); + } + + return results.ToArray(); + } + + var pending = new List>(); + + foreach (var task in tasks) + { + pending.Add(task); + + if (pending.Count < concurrencyLimit) + { + continue; + } + + var completed = await Task.WhenAny(pending); + pending.Remove(completed); + results.Add(await completed); + } + + results.AddRange(await Task.WhenAll(pending)); + + return results.ToArray(); + } + } +} diff --git a/tools/pipeline-owners-extractor/Azure.Sdk.Tools.PipelineOwnersExtractor/GitHubService.cs b/tools/pipeline-owners-extractor/Azure.Sdk.Tools.PipelineOwnersExtractor/GitHubService.cs new file mode 100644 index 00000000000..c12ca0c75da --- /dev/null +++ b/tools/pipeline-owners-extractor/Azure.Sdk.Tools.PipelineOwnersExtractor/GitHubService.cs @@ -0,0 +1,74 @@ +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Linq; +using System.Net.Http; +using System.Threading.Tasks; +using Azure.Sdk.Tools.CodeOwnersParser; +using Microsoft.Extensions.Logging; + +namespace Azure.Sdk.Tools.PipelineOwnersExtractor +{ + /// + /// Interface for interacting with GitHub + /// + public class GitHubService + { + private static readonly HttpClient httpClient = new HttpClient(); + + private readonly ILogger logger; + private readonly ConcurrentDictionary> codeOwnersFileCache; + + /// + /// Creates a new GitHubService + /// + /// Logger + public GitHubService(ILogger logger) + { + this.logger = logger; + this.codeOwnersFileCache = new ConcurrentDictionary>(); + } + + /// + /// Looks for CODEOWNERS in the main branch of the given repo URL using cache + /// + /// GitHub repository URL + /// Contents fo the located CODEOWNERS file + public async Task> GetCodeOwnersFile(Uri repoUrl) + { + List result; + if (codeOwnersFileCache.TryGetValue(repoUrl.ToString(), out result)) + { + return result; + } + + result = await GetCodeownersFileImpl(repoUrl); + codeOwnersFileCache.TryAdd(repoUrl.ToString(), result); + return result; + } + + /// + /// Looks for CODEOWNERS in the main branch of the given repo URL + /// + /// + /// + private async Task> GetCodeownersFileImpl(Uri repoUrl) + { + // Gets the repo path from the URL + var relevantPathParts = repoUrl.Segments.Skip(1).Take(2); + var repoPath = string.Join("", relevantPathParts); + + var codeOwnersUrl = $"https://raw.githubusercontent.com/{repoPath}/main/.github/CODEOWNERS"; + var result = await httpClient.GetAsync(codeOwnersUrl); + if (result.IsSuccessStatusCode) + { + this.logger.LogInformation("Retrieved CODEOWNERS file URL = {0}", codeOwnersUrl); + return CodeOwnersFile.ParseContent(await result.Content.ReadAsStringAsync()); + } + + this.logger.LogWarning("Could not retrieve CODEOWNERS file URL = {0} ResponseCode = {1}", codeOwnersUrl, result.StatusCode); + return default; + } + + } +} diff --git a/tools/pipeline-owners-extractor/Azure.Sdk.Tools.PipelineOwnersExtractor/Processor.cs b/tools/pipeline-owners-extractor/Azure.Sdk.Tools.PipelineOwnersExtractor/Processor.cs new file mode 100644 index 00000000000..4a928e1b59f --- /dev/null +++ b/tools/pipeline-owners-extractor/Azure.Sdk.Tools.PipelineOwnersExtractor/Processor.cs @@ -0,0 +1,158 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text.RegularExpressions; +using System.Threading; +using System.Threading.Tasks; + +using Azure.Sdk.Tools.CodeOwnersParser; +using Azure.Sdk.Tools.NotificationConfiguration.Helpers; +using Azure.Sdk.Tools.NotificationConfiguration.Services; +using Azure.Sdk.Tools.PipelineOwnersExtractor.Configuration; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using Microsoft.TeamFoundation.Build.WebApi; + +using Newtonsoft.Json; + +namespace Azure.Sdk.Tools.PipelineOwnersExtractor +{ + public class Processor + { + // Type 2 maps to a pipeline YAML file in the repository + private const int PipelineYamlProcessType = 2; + + private readonly ILogger logger; + private readonly GitHubToAADConverter githubToAadResolver; + private readonly AzureDevOpsService devOpsService; + private readonly GitHubService gitHubService; + private readonly PipelineOwnerSettings settings; + + public Processor( + ILogger logger, + GitHubService gitHubService, + GitHubToAADConverter githubToAadResolver, + AzureDevOpsService devOpsService, + IOptions options) + { + this.logger = logger; + this.gitHubService = gitHubService; + this.githubToAadResolver = githubToAadResolver; + this.devOpsService = devOpsService; + this.settings = options.Value; + } + + public async Task ExecuteAsync(CancellationToken stoppingToken = default) + { + var projects = this.settings.Projects.Split(','); + var pipelineResults = await Task.WhenAll(projects.Select(x => devOpsService.GetPipelinesAsync(x.Trim()))); + + // flatten arrays of pipelines by project into an array of pipelines + var pipelines = pipelineResults + .SelectMany(x => x) + .ToArray(); + + var repositoryUrls = GetDistinctRepositoryUrls(pipelines); + + var codeOwnerEntriesByRepository = await GetCodeOwnerEntriesAsync(repositoryUrls); + + var pipelineOwners = await AssociateOwnersToPipelinesAsync(pipelines, codeOwnerEntriesByRepository); + + var outputContent = pipelineOwners.ToDictionary(x => x.Pipeline.Id, x => x.Owners); + + await File.WriteAllTextAsync(this.settings.Output, JsonConvert.SerializeObject(outputContent, Formatting.Indented), stoppingToken); + } + + private async Task Owners)>> AssociateOwnersToPipelinesAsync( + IEnumerable pipelines, + Dictionary> codeOwnerEntriesByRepository) + { + var linkedGithubUsers = await githubToAadResolver.GetPeopleLinksAsync(); + + var microsoftAliasMap = linkedGithubUsers.ToDictionary(x => x.GitHub.Login, x => x.Aad.UserPrincipalName, StringComparer.OrdinalIgnoreCase); + + var microsoftPipelineOwners = new List<(BuildDefinition Pipeline, List Owners)>(); + + var unrecognizedGitHubAliases = new HashSet(); + + foreach (var pipeline in pipelines) + { + if (pipeline.Process.Type != PipelineYamlProcessType || !(pipeline.Process is YamlProcess process)) + { + logger.LogInformation("Skipping non-yaml pipeline '{Pipeline}'", pipeline.Name); + continue; + } + + if (pipeline.Repository.Type != "GitHub") + { + logger.LogInformation("Skipping pipeline '{Pipeline}' with {Type} repository", pipeline.Name, pipeline.Repository.Type); + continue; + } + + if (!codeOwnerEntriesByRepository.TryGetValue(SanitizeRepositoryUrl(pipeline.Repository.Url.AbsoluteUri), out var codeOwnerEntries)) + { + logger.LogInformation("Skipping pipeline '{Pipeline}' because its repo has no CODEOWNERS file", pipeline.Name); + continue; + } + + logger.LogInformation("Processing pipeline '{Pipeline}'", pipeline.Name); + + logger.LogInformation("Searching CODEOWNERS for patch matching {Path}", process.YamlFilename); + var codeOwnerEntry = CodeOwnersFile.FindOwnersForClosestMatch(codeOwnerEntries, process.YamlFilename); + codeOwnerEntry.FilterOutNonUserAliases(); + + logger.LogInformation("Matching Path = {Path}, Owner Count = {OwnerCount}", process.YamlFilename, codeOwnerEntry.Owners.Count); + + // Get set of team members in the CODEOWNERS file + var githubOwners = codeOwnerEntry.Owners.ToArray(); + + var microsoftOwners = new List(); + + foreach (var githubOwner in githubOwners) + { + if (microsoftAliasMap.TryGetValue(githubOwner, out var microsoftOwner)) + { + microsoftOwners.Add(microsoftOwner); + } + else + { + unrecognizedGitHubAliases.Add(githubOwner); + } + } + + microsoftPipelineOwners.Add((pipeline, microsoftOwners)); + } + + var mappedCount = microsoftPipelineOwners.SelectMany(x => x.Owners).Distinct().Count(); + logger.LogInformation("{Mapped} unique pipeline owner aliases mapped to Microsoft users. {Unmapped} could not be mapped.", mappedCount, unrecognizedGitHubAliases.Count); + + return microsoftPipelineOwners; + } + + private async Task>> GetCodeOwnerEntriesAsync(string[] repositoryUrls) + { + var tasks = repositoryUrls + .Select(SanitizeRepositoryUrl) + .Select(async url => ( + RepositoryUrl: url, + CodeOwners: await this.gitHubService.GetCodeOwnersFile(new Uri(url)) + )); + + var taskResults = await Task.WhenAll(tasks); + + return taskResults.Where(x => x.CodeOwners != null) + .ToDictionary(x => x.RepositoryUrl, x => x.CodeOwners, StringComparer.OrdinalIgnoreCase); + } + + private static string SanitizeRepositoryUrl(string url) => Regex.Replace(url, @"\.git$", string.Empty); + + private static string[] GetDistinctRepositoryUrls(IEnumerable pipelines) + { + return pipelines.Where(x => x.Repository?.Properties?.ContainsKey("manageUrl") == true) + .Select(x => x.Repository.Properties["manageUrl"]) + .Distinct() + .ToArray(); + } + } +} diff --git a/tools/pipeline-owners-extractor/Azure.Sdk.Tools.PipelineOwnersExtractor/Program.cs b/tools/pipeline-owners-extractor/Azure.Sdk.Tools.PipelineOwnersExtractor/Program.cs new file mode 100644 index 00000000000..07cc6c3f4de --- /dev/null +++ b/tools/pipeline-owners-extractor/Azure.Sdk.Tools.PipelineOwnersExtractor/Program.cs @@ -0,0 +1,68 @@ +using System; +using System.Threading.Tasks; +using Azure.Core; +using Azure.Identity; +using Azure.Sdk.Tools.NotificationConfiguration.Helpers; +using Azure.Sdk.Tools.NotificationConfiguration.Services; +using Azure.Sdk.Tools.PipelineOwnersExtractor.Configuration; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using Microsoft.VisualStudio.Services.Common; +using Microsoft.VisualStudio.Services.WebApi; + +namespace Azure.Sdk.Tools.PipelineOwnersExtractor +{ + public class Program + { + public static async Task Main(string[] args) + { + Console.WriteLine("Initializing PipelineOwnersExtractor"); + + using var host = Host.CreateDefaultBuilder(args) + .ConfigureServices((context, services) => + { + services.AddSingleton(); + services.AddSingleton(); + services.Configure(context.Configuration); + services.AddSingleton, PostConfigureKeyVaultSettings>(); + services.AddSingleton(); + services.AddSingleton(CreateGithubAadConverter); + services.AddSingleton(CreateAzureDevOpsService); + services.AddSingleton(); + }) + .Build(); + + var processor = host.Services.GetRequiredService(); + + await processor.ExecuteAsync(); + } + + private static AzureDevOpsService CreateAzureDevOpsService(IServiceProvider provider) + { + var logger = provider.GetRequiredService>(); + var settings = provider.GetRequiredService>().Value; + + var uri = new Uri($"https://dev.azure.com/{settings.Account}"); + var credentials = new VssBasicCredential("pat", settings.AzureDevOpsPat); + var connection = new VssConnection(uri, credentials); + + return new AzureDevOpsService(connection, logger); + } + + private static GitHubToAADConverter CreateGithubAadConverter(IServiceProvider provider) + { + var logger = provider.GetRequiredService>(); + var settings = provider.GetRequiredService>().Value; + + var credential = new ClientSecretCredential( + settings.OpenSourceAadTenantId, + settings.OpenSourceAadAppId, + settings.OpenSourceAadSecret); + + return new GitHubToAADConverter(credential, logger); + } + } +} diff --git a/tools/pipeline-owners-extractor/Azure.Sdk.Tools.PipelineOwnersExtractor/appsettings.json b/tools/pipeline-owners-extractor/Azure.Sdk.Tools.PipelineOwnersExtractor/appsettings.json new file mode 100644 index 00000000000..c5bd54b8af4 --- /dev/null +++ b/tools/pipeline-owners-extractor/Azure.Sdk.Tools.PipelineOwnersExtractor/appsettings.json @@ -0,0 +1,17 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Warning", + "Microsoft.Hosting": "Information", + "Azure.Sdk.Tools": "Information", + "Azure.Core": "Error" + } + }, + "Account": "azure-sdk", + "Projects": "internal,public", + "OpenSourceAadAppId": "https://AzureSDKEngKeyVault.vault.azure.net/secrets/opensource-aad-app-id", + "OpenSourceAadSecret": "https://AzureSDKEngKeyVault.vault.azure.net/secrets/opensource-aad-secret", + "OpenSourceAadTenantId": "https://AzureSDKEngKeyVault.vault.azure.net/secrets/opensource-aad-tenant-id", + "AzureDevOpsPat": "https://AzureSDKEngKeyVault.vault.azure.net/secrets/azure-sdk-notification-tools-pat", + "Output": "pipelineOwners.json" +} diff --git a/tools/pipeline-owners-extractor/PipelineOwnersExtractor.sln b/tools/pipeline-owners-extractor/PipelineOwnersExtractor.sln new file mode 100644 index 00000000000..cc454c2528c --- /dev/null +++ b/tools/pipeline-owners-extractor/PipelineOwnersExtractor.sln @@ -0,0 +1,37 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.2.32602.215 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "identity-resolution", "..\identity-resolution\identity-resolution.csproj", "{54513498-7FA1-4525-915B-405746FBDDF5}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Azure.Sdk.Tools.CodeOwnersParser", "..\code-owners-parser\CodeOwnersParser\Azure.Sdk.Tools.CodeOwnersParser.csproj", "{F0516B1D-037F-4096-9932-ED8F180EBC5D}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Azure.Sdk.Tools.PipelineOwnersExtractor", "Azure.Sdk.Tools.PipelineOwnersExtractor\Azure.Sdk.Tools.PipelineOwnersExtractor.csproj", "{4813A8E4-BC83-4F2D-8C87-F9AAAA11B207}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {54513498-7FA1-4525-915B-405746FBDDF5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {54513498-7FA1-4525-915B-405746FBDDF5}.Debug|Any CPU.Build.0 = Debug|Any CPU + {54513498-7FA1-4525-915B-405746FBDDF5}.Release|Any CPU.ActiveCfg = Release|Any CPU + {54513498-7FA1-4525-915B-405746FBDDF5}.Release|Any CPU.Build.0 = Release|Any CPU + {F0516B1D-037F-4096-9932-ED8F180EBC5D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {F0516B1D-037F-4096-9932-ED8F180EBC5D}.Debug|Any CPU.Build.0 = Debug|Any CPU + {F0516B1D-037F-4096-9932-ED8F180EBC5D}.Release|Any CPU.ActiveCfg = Release|Any CPU + {F0516B1D-037F-4096-9932-ED8F180EBC5D}.Release|Any CPU.Build.0 = Release|Any CPU + {4813A8E4-BC83-4F2D-8C87-F9AAAA11B207}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {4813A8E4-BC83-4F2D-8C87-F9AAAA11B207}.Debug|Any CPU.Build.0 = Debug|Any CPU + {4813A8E4-BC83-4F2D-8C87-F9AAAA11B207}.Release|Any CPU.ActiveCfg = Release|Any CPU + {4813A8E4-BC83-4F2D-8C87-F9AAAA11B207}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {B35109A5-A9DD-4251-9FC6-EF3D371B894A} + EndGlobalSection +EndGlobal diff --git a/tools/pipeline-owners-extractor/ci.yml b/tools/pipeline-owners-extractor/ci.yml new file mode 100644 index 00000000000..dcef7d951b7 --- /dev/null +++ b/tools/pipeline-owners-extractor/ci.yml @@ -0,0 +1,27 @@ +# NOTE: Please refer to https://aka.ms/azsdk/engsys/ci-yaml before editing this file. +trigger: + branches: + include: + - main + - feature/* + - release/* + - hotfix/* + paths: + include: + - tools/pipeline-owners-extractor + +pr: + branches: + include: + - main + - feature/* + - release/* + - hotfix/* + paths: + include: + - tools/pipeline-owners-extractor + +extends: + template: /eng/pipelines/templates/stages/archetype-sdk-tool-dotnet.yml + parameters: + ToolDirectory: tools/pipeline-owners-extractor