Add MSBuild task for creating/updating pull requests
This commit is contained in:
parent
1f596e127b
commit
1bf9e05dfd
|
|
@ -1,7 +1,7 @@
|
|||
|
||||
Microsoft Visual Studio Solution File, Format Version 12.00
|
||||
# Visual Studio 15
|
||||
VisualStudioVersion = 15.0.26730.0
|
||||
VisualStudioVersion = 15.0.26730.16
|
||||
MinimumVisualStudioVersion = 15.0.26124.0
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Templates.Test", "test\Templates.Test\Templates.Test.csproj", "{D43A4D24-D514-44C2-9438-54F6EDF58680}"
|
||||
EndProject
|
||||
|
|
@ -23,6 +23,10 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.DotNet.Web.Client
|
|||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.DotNet.Web.ProjectTemplates.2.1", "src\Microsoft.DotNet.Web.ProjectTemplates.2.1\Microsoft.DotNet.Web.ProjectTemplates.2.1.csproj", "{260EBA09-DEF5-429C-99BF-90CA1456A576}"
|
||||
EndProject
|
||||
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Tools", "Tools", "{973DC5B6-710B-4FC8-AF20-E94B93859DE8}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PullRequestSubmitter", "tools\PullRequestSubmitter\PullRequestSubmitter.csproj", "{AFF8B079-5BA1-4DA8-9EAF-BEC8414F889A}"
|
||||
EndProject
|
||||
Global
|
||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||
Debug|Any CPU = Debug|Any CPU
|
||||
|
|
@ -65,6 +69,10 @@ Global
|
|||
{260EBA09-DEF5-429C-99BF-90CA1456A576}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{260EBA09-DEF5-429C-99BF-90CA1456A576}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{260EBA09-DEF5-429C-99BF-90CA1456A576}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{AFF8B079-5BA1-4DA8-9EAF-BEC8414F889A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{AFF8B079-5BA1-4DA8-9EAF-BEC8414F889A}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{AFF8B079-5BA1-4DA8-9EAF-BEC8414F889A}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{AFF8B079-5BA1-4DA8-9EAF-BEC8414F889A}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
EndGlobalSection
|
||||
GlobalSection(SolutionProperties) = preSolution
|
||||
HideSolutionNode = FALSE
|
||||
|
|
@ -73,10 +81,12 @@ Global
|
|||
{ABC9D95C-7FBD-4F8D-827A-00942EA3D0C0} = {0AD6E692-E423-408C-B523-DAFB19412E4B}
|
||||
{B8EB8821-9B58-465A-9693-5F9289AA7B29} = {0AD6E692-E423-408C-B523-DAFB19412E4B}
|
||||
{62D00388-8824-4661-8CC8-8D8436FF97E6} = {0AD6E692-E423-408C-B523-DAFB19412E4B}
|
||||
{15806338-550D-4418-99AE-42FDAE03808A} = {973DC5B6-710B-4FC8-AF20-E94B93859DE8}
|
||||
{01E12D5E-8540-4BC8-9A54-41EDD55E762E} = {0AD6E692-E423-408C-B523-DAFB19412E4B}
|
||||
{402E62D1-7FD0-4E07-812C-0E385D98D6D9} = {0AD6E692-E423-408C-B523-DAFB19412E4B}
|
||||
{1731F6D9-1DFC-49D6-8F28-471194B1962C} = {0AD6E692-E423-408C-B523-DAFB19412E4B}
|
||||
{260EBA09-DEF5-429C-99BF-90CA1456A576} = {0AD6E692-E423-408C-B523-DAFB19412E4B}
|
||||
{AFF8B079-5BA1-4DA8-9EAF-BEC8414F889A} = {973DC5B6-710B-4FC8-AF20-E94B93859DE8}
|
||||
EndGlobalSection
|
||||
GlobalSection(ExtensibilityGlobals) = postSolution
|
||||
SolutionGuid = {E9B27B0D-4F85-431B-9C26-80CFE4393D36}
|
||||
|
|
|
|||
|
|
@ -1,10 +1,41 @@
|
|||
<Project>
|
||||
<Import Project="FixPackageOutputDirs.targets" />
|
||||
|
||||
<UsingTask
|
||||
TaskName="PullRequestSubmitter.PullRequestTask"
|
||||
AssemblyFile="$(MSBuildThisFileDirectory)..\tools\PullRequestSubmitter\bin\Debug\netcoreapp2.0\PullRequestSubmitter.dll" />
|
||||
|
||||
<Target Name="PushNuGetPackages">
|
||||
<ItemGroup>
|
||||
<PackagesToPublish Include="$(BuildDir)*.nupkg" />
|
||||
</ItemGroup>
|
||||
<PushNuGetPackages Packages="@(PackagesToPublish)" Feed="$(NuGetPublishFeed)" ApiKey="$(ApiKey)" />
|
||||
</Target>
|
||||
|
||||
<Target Name="SendPullRequestToCliRepo">
|
||||
<PropertyGroup>
|
||||
<!-- Read the ASP.NET Core package version from a .deps.json file, because
|
||||
the CI server doesn't otherwise have that information to supply -->
|
||||
<DepsFilePath>$(MSBuildThisFileDirectory)..\tools\DependencyUpdater\bin\Debug\netstandard2.0\DependencyUpdater.deps.json</DepsFilePath>
|
||||
<DepsFileContent>$([System.IO.File]::ReadAllText('$(DepsFilePath)'))</DepsFileContent>
|
||||
<AspNetCoreRuntimePackageVersion>$([System.Text.RegularExpressions.Regex]::Match($(DepsFileContent), `\s+"Microsoft.AspNetCore": "([^"]+)"`).Groups[1].Value)</AspNetCoreRuntimePackageVersion>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PropertyUpdate Include="AspNetCoreRuntimePackageFolderName" NewValue="$(AspNetCoreRuntimePackageFolderName)" />
|
||||
<PropertyUpdate Include="AspNetCoreRuntimePackageVersion" NewValue="$(AspNetCoreRuntimePackageVersion)" />
|
||||
</ItemGroup>
|
||||
|
||||
<PullRequestTask
|
||||
ApiToken="$(GitHubApiToken)"
|
||||
UpstreamRepoOwner="$(UpstreamRepoOwner)"
|
||||
UpstreamRepoName="$(UpstreamRepoName)"
|
||||
UpstreamRepoBranch="$(UpstreamRepoBranch)"
|
||||
ForkRepoName="$(ForkRepoName)"
|
||||
ForkRepoBranch="$(ForkRepoBranch)"
|
||||
Message="Update ASP.NET Core to $(AspNetCoreRuntimePackageVersion)"
|
||||
FileToUpdate="build/DependencyVersions.props"
|
||||
PropertyUpdates="@(PropertyUpdate)" />
|
||||
</Target>
|
||||
|
||||
</Project>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,161 @@
|
|||
using Octokit;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Text.RegularExpressions;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace PullRequestSubmitter.Helpers
|
||||
{
|
||||
static class GitHubUtil
|
||||
{
|
||||
public static async Task<IDictionary<string, NewBlob>> GetEditsToCommit(
|
||||
GitHubClient client, Repository upstreamRepo, string baseSha,
|
||||
IEnumerable<PropertyUpdate> propertyUpdates)
|
||||
{
|
||||
// Find the file to update
|
||||
var existingTree = await client.Git.Tree.GetRecursive(upstreamRepo.Id, baseSha);
|
||||
|
||||
// Update the files' contents
|
||||
var result = new Dictionary<string, NewBlob>();
|
||||
var filesToUpdate = propertyUpdates.GroupBy(p => p.Filename);
|
||||
foreach (var fileToUpdate in filesToUpdate)
|
||||
{
|
||||
var fileContents = await GetFileContentsAsString(
|
||||
client, upstreamRepo, existingTree.Tree, fileToUpdate.Key);
|
||||
|
||||
foreach (var propToUpdate in fileToUpdate)
|
||||
{
|
||||
var propName = propToUpdate.PropertyName;
|
||||
var patternToReplace = new Regex($"<{propName}>[^<]+</{propName}>");
|
||||
if (!patternToReplace.IsMatch(fileContents))
|
||||
{
|
||||
throw new Exception($"The file {fileToUpdate.Key} does not contain a match for regex " + patternToReplace.ToString());
|
||||
}
|
||||
|
||||
fileContents = patternToReplace.Replace(
|
||||
fileContents,
|
||||
$"<{propName}>{propToUpdate.NewValue}</{propName}>");
|
||||
}
|
||||
|
||||
var newBlob = new NewBlob { Content = fileContents, Encoding = EncodingType.Utf8 };
|
||||
result.Add(fileToUpdate.Key, newBlob);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
public static async Task<string> GetLatestCommitSha(
|
||||
GitHubClient client, Repository repo, string branchName)
|
||||
{
|
||||
var commitRef = await client.Git.Reference.Get(
|
||||
repo.Id,
|
||||
$"heads/{branchName}");
|
||||
return commitRef.Object.Sha;
|
||||
}
|
||||
|
||||
public static async Task<string> CommitModifiedFiles(
|
||||
GitHubClient client, Repository toRepo, string toBranchName, string parentCommitSha,
|
||||
IDictionary<string, NewBlob> modifiedFiles, string commitMessage)
|
||||
{
|
||||
// Build and commit a new tree representing the updated state
|
||||
var newTree = new NewTree { BaseTree = parentCommitSha };
|
||||
foreach (var kvp in modifiedFiles)
|
||||
{
|
||||
newTree.Tree.Remove(new NewTreeItem { Path = kvp.Key });
|
||||
newTree.Tree.Add(new NewTreeItem()
|
||||
{
|
||||
Type = TreeType.Blob,
|
||||
Mode = "100644",
|
||||
Sha = (await client.Git.Blob.Create(toRepo.Id, kvp.Value)).Sha,
|
||||
Path = kvp.Key
|
||||
});
|
||||
}
|
||||
var createdTree = await client.Git.Tree.Create(toRepo.Id, newTree);
|
||||
var commit = await client.Git.Commit.Create(
|
||||
toRepo.Id,
|
||||
new NewCommit(commitMessage, createdTree.Sha, parentCommitSha));
|
||||
|
||||
// Update the target branch to point to the new commit
|
||||
await client.Git.Reference.Update(
|
||||
toRepo.Id,
|
||||
$"heads/{toBranchName}",
|
||||
new ReferenceUpdate(commit.Sha, force: true));
|
||||
|
||||
return commit.Sha;
|
||||
}
|
||||
|
||||
public static async Task<Issue> FindExistingPullRequestToUpdate(
|
||||
GitHubClient client, User currentUser, Repository upstreamRepo,
|
||||
Repository forkRepo, string forkBranch)
|
||||
{
|
||||
// Search for candidate PRs (same author, still open, etc.)
|
||||
var fromBaseRef = $"{forkRepo.Owner.Login}:{forkBranch}";
|
||||
var searchInRepos = new RepositoryCollection();
|
||||
searchInRepos.Add(upstreamRepo.Owner.Login, upstreamRepo.Name);
|
||||
var searchRequest = new SearchIssuesRequest
|
||||
{
|
||||
Repos = searchInRepos,
|
||||
Type = IssueTypeQualifier.PullRequest,
|
||||
Author = currentUser.Login,
|
||||
State = ItemState.Open
|
||||
};
|
||||
var searchResults = await client.Search.SearchIssues(searchRequest);
|
||||
|
||||
// Of the candidates, find the highest-numbered one that is requesting a
|
||||
// pull from the same fork and branch. GitHub only allows there to be one
|
||||
// of these at any given time, but we're more likely to find it faster
|
||||
// by searching from newest to oldest.
|
||||
var candidates = searchResults.Items.OrderByDescending(item => item.Number);
|
||||
foreach (var prInfo in candidates)
|
||||
{
|
||||
var pr = await client.PullRequest.Get(upstreamRepo.Id, prInfo.Number);
|
||||
if (pr.Head?.Repository?.Id == forkRepo.Id && pr.Head?.Ref == forkBranch)
|
||||
{
|
||||
return prInfo;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public static async Task<PullRequest> CreateNewPullRequest(
|
||||
GitHubClient client, Repository upstreamRepo, string upstreamBranch,
|
||||
Repository forkRepo, string forkBranch, string prBodyText)
|
||||
{
|
||||
var fromBaseRef = $"{forkRepo.Owner.Login}:{forkBranch}";
|
||||
var newPr = new NewPullRequest(
|
||||
prBodyText,
|
||||
fromBaseRef,
|
||||
upstreamBranch);
|
||||
return await client.PullRequest.Create(upstreamRepo.Id, newPr);
|
||||
}
|
||||
|
||||
public static async Task UpdateExistingPullRequestTitle(
|
||||
GitHubClient client, Repository upstreamRepo, int prNumber, string newTitle)
|
||||
{
|
||||
var updateInfo = new PullRequestUpdate { Title = newTitle };
|
||||
await client.PullRequest.Update(upstreamRepo.Id, prNumber, updateInfo);
|
||||
}
|
||||
|
||||
private static async Task<string> GetFileContentsAsString(
|
||||
GitHubClient client, Repository repo, IReadOnlyList<TreeItem> tree, string path)
|
||||
{
|
||||
var existingFile = tree.FirstOrDefault(item => item.Path == path);
|
||||
var blob = await client.Git.Blob.Get(repo.Id, existingFile.Sha);
|
||||
|
||||
switch (blob.Encoding.Value)
|
||||
{
|
||||
case EncodingType.Utf8:
|
||||
return blob.Content;
|
||||
case EncodingType.Base64:
|
||||
return Encoding.UTF8.GetString(Convert.FromBase64String(blob.Content));
|
||||
default:
|
||||
throw new InvalidDataException($"Unsupported encoding: {blob.Encoding.StringValue}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
namespace PullRequestSubmitter.Helpers
|
||||
{
|
||||
class PropertyUpdate
|
||||
{
|
||||
public string Filename { get; set; }
|
||||
public string PropertyName { get; set; }
|
||||
public string NewValue { get; set; }
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,14 @@
|
|||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>netcoreapp2.0</TargetFramework>
|
||||
<CopyLocalLockFileAssemblies>true</CopyLocalLockFileAssemblies>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.Build.Framework" />
|
||||
<PackageReference Include="Microsoft.Build.Utilities.Core" />
|
||||
<PackageReference Include="Octokit" Version="0.26.0" NoWarn="KRB4002" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
|
@ -0,0 +1,105 @@
|
|||
using Microsoft.Build.Framework;
|
||||
using Octokit;
|
||||
using PullRequestSubmitter.Helpers;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace PullRequestSubmitter
|
||||
{
|
||||
public class PullRequestTask : Microsoft.Build.Utilities.Task
|
||||
{
|
||||
[Required] public string ApiToken { get; set; }
|
||||
[Required] public string UpstreamRepoOwner { get; set; }
|
||||
[Required] public string UpstreamRepoName { get; set; }
|
||||
[Required] public string UpstreamRepoBranch { get; set; }
|
||||
[Required] public string ForkRepoName { get; set; }
|
||||
[Required] public string ForkRepoBranch { get; set; }
|
||||
[Required] public string Message { get; set; }
|
||||
[Required] public string FileToUpdate { get; set; }
|
||||
[Required] public ITaskItem[] PropertyUpdates { get; set; }
|
||||
|
||||
public override bool Execute()
|
||||
{
|
||||
return ExecuteAsync().Result;
|
||||
}
|
||||
|
||||
private IEnumerable<PropertyUpdate> GetPropertyUpdates()
|
||||
{
|
||||
return PropertyUpdates.Select(item => new PropertyUpdate
|
||||
{
|
||||
Filename = FileToUpdate,
|
||||
PropertyName = item.ItemSpec,
|
||||
NewValue = item.GetMetadata("NewValue")
|
||||
});
|
||||
}
|
||||
|
||||
private async Task<bool> ExecuteAsync()
|
||||
{
|
||||
var client = new GitHubClient(new ProductHeaderValue("PullRequestSubmitter"))
|
||||
{
|
||||
Credentials = new Credentials(ApiToken),
|
||||
};
|
||||
|
||||
// Find the upstream repo and determine what edits we want to make
|
||||
LogHigh($"Finding upstream repo: {UpstreamRepoOwner}/{UpstreamRepoName}...");
|
||||
var upstreamRepo = await client.Repository.Get(UpstreamRepoOwner, UpstreamRepoName);
|
||||
var upstreamCommitSha = await GitHubUtil.GetLatestCommitSha(client, upstreamRepo, UpstreamRepoBranch);
|
||||
LogHigh($"Found upstream commit to update: {upstreamCommitSha} ({UpstreamRepoBranch})");
|
||||
var editsToCommit = await GitHubUtil.GetEditsToCommit(
|
||||
client, upstreamRepo, upstreamCommitSha, GetPropertyUpdates());
|
||||
if (editsToCommit.Any())
|
||||
{
|
||||
var filesList = string.Join('\n',
|
||||
editsToCommit.Select(e => " - " + e.Key));
|
||||
LogHigh($"Will apply edits to file(s):\n{filesList}");
|
||||
}
|
||||
else
|
||||
{
|
||||
Log.LogError("Found no edits to apply. Aborting.");
|
||||
return false;
|
||||
}
|
||||
|
||||
// Commit the edits into the fork repo, updating its head to point to a new tree
|
||||
// formed by updating the tree from the upstream SHA
|
||||
var currentUser = await client.User.Current();
|
||||
LogHigh($"Finding fork repo: {currentUser.Login}/{ForkRepoName}...");
|
||||
var forkRepo = await client.Repository.Get(currentUser.Login, ForkRepoName);
|
||||
var newCommitSha = await GitHubUtil.CommitModifiedFiles(
|
||||
client,
|
||||
forkRepo,
|
||||
ForkRepoBranch,
|
||||
upstreamCommitSha,
|
||||
editsToCommit,
|
||||
Message);
|
||||
LogHigh($"Committed edits. {currentUser.Login}/{ForkRepoName} branch {ForkRepoBranch} is now at {newCommitSha}");
|
||||
|
||||
// If applicable, submit a new PR
|
||||
LogHigh($"Checking if there is already an open PR we can update...");
|
||||
var prToUpdate = await GitHubUtil.FindExistingPullRequestToUpdate(
|
||||
client, currentUser, upstreamRepo, forkRepo, ForkRepoBranch);
|
||||
if (prToUpdate == null)
|
||||
{
|
||||
LogHigh($"No existing open PR found. Creating new PR...");
|
||||
var newPr = await GitHubUtil.CreateNewPullRequest(
|
||||
client, upstreamRepo, UpstreamRepoBranch, forkRepo, ForkRepoBranch, Message);
|
||||
LogHigh($"Created pull request #{newPr.Number} at {newPr.HtmlUrl}");
|
||||
}
|
||||
else
|
||||
{
|
||||
LogHigh($"Found existing PR #{prToUpdate.Number}. Updating details...");
|
||||
await GitHubUtil.UpdateExistingPullRequestTitle(
|
||||
client, upstreamRepo, prToUpdate.Number, Message);
|
||||
LogHigh($"Finished updating PR #{prToUpdate.Number} at {prToUpdate.HtmlUrl}");
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private void LogHigh(string message)
|
||||
{
|
||||
Log.LogMessage(MessageImportance.High, message);
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue