-
Notifications
You must be signed in to change notification settings - Fork 67
Add initial implementation of Microsoft.DotNet.GitAutomation library
#2158
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
17 commits
Select commit
Hold shift + click to select a range
198af3c
Add Automation library
lbussell 082932a
Update README for Automation library
lbussell c21bf4e
Handle scenario where source branch exists but there is no PR
lbussell 7c634c9
Update test file name
lbussell 82ab924
Clone PR source repo when appending
lbussell b83d51a
Narrow Automation public API surface
lbussell afc5417
Require XML docs for Automation public API
lbussell 9b6d4cc
Add dependency injection support to Automation
lbussell aaa6c29
Validate pull request key names
lbussell 4f1f171
Rename Automation library to GitAutomation
lbussell eacbda2
Remove superfluous tests
lbussell 3e79d8c
Use standard logging registration
lbussell 3d65b30
Remove redundant logging abstractions reference
lbussell e97e1b3
Version GitAutomation independently
lbussell 10406a9
Trigger package builds for GitAutomation
lbussell f3f8569
Include GitAutomation README in package
lbussell 1cc27f4
Merge branch 'main' into lbussell/automation-code
lbussell File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,77 @@ | ||
| // Licensed to the .NET Foundation under one or more agreements. | ||
| // The .NET Foundation licenses this file to you under the MIT license. | ||
| // See the LICENSE file in the project root for more information. | ||
|
|
||
| using Microsoft.Extensions.DependencyInjection; | ||
| using Microsoft.Extensions.Logging; | ||
| using Microsoft.Extensions.Logging.Abstractions; | ||
|
|
||
| namespace Microsoft.DotNet.GitAutomation.Tests; | ||
|
|
||
| [TestClass] | ||
| public sealed class DependencyInjectionTests | ||
| { | ||
| [TestMethod] | ||
| public void PullRequestManager_ResolvesWithDefaultServices() | ||
| { | ||
| ServiceCollection services = new(); | ||
| services.AddPullRequestAutomation( | ||
| new AutomationIdentity("bot", "bot@example.com"), | ||
| "token"); | ||
|
|
||
| using ServiceProvider serviceProvider = services.BuildServiceProvider(); | ||
|
|
||
| PullRequestManager manager = serviceProvider.GetRequiredService<PullRequestManager>(); | ||
|
|
||
| Assert.IsNotNull(manager); | ||
| } | ||
|
|
||
| [TestMethod] | ||
| public void PullRequestManager_ResolvesWithCustomServices() | ||
| { | ||
| ServiceCollection services = new(); | ||
| bool processRunnerResolved = false; | ||
| bool accessTokenProviderResolved = false; | ||
| services.AddSingleton<IProcessRunner>(_ => | ||
| { | ||
| processRunnerResolved = true; | ||
| return new StubProcessRunner(); | ||
| }); | ||
| services.AddSingleton<IGitAccessTokenProvider>(_ => | ||
| { | ||
| accessTokenProviderResolved = true; | ||
| return new StaticGitAccessTokenProvider("token"); | ||
| }); | ||
| services.AddSingleton<ILoggerFactory>(NullLoggerFactory.Instance); | ||
| services.AddSingleton(new AutomationIdentity("bot", "bot@example.com")); | ||
| services.AddSingleton<PullRequestManager>(); | ||
|
|
||
| using ServiceProvider serviceProvider = services.BuildServiceProvider(); | ||
|
|
||
| PullRequestManager manager = serviceProvider.GetRequiredService<PullRequestManager>(); | ||
|
|
||
| Assert.IsNotNull(manager); | ||
| Assert.IsTrue(processRunnerResolved); | ||
| Assert.IsTrue(accessTokenProviderResolved); | ||
| } | ||
|
|
||
| [TestMethod] | ||
| public void PullRequestManager_CanBeCreatedWithoutDependencyInjection() | ||
| { | ||
| PullRequestManager manager = new( | ||
| "token", | ||
| new AutomationIdentity("bot", "bot@example.com")); | ||
|
|
||
| Assert.IsNotNull(manager); | ||
| } | ||
|
|
||
| private sealed class StubProcessRunner : IProcessRunner | ||
| { | ||
| public Task<ProcessResult> RunAsync( | ||
| string? workingDirectory, | ||
| string fileName, | ||
| IEnumerable<string> arguments, | ||
| CancellationToken cancellationToken) => | ||
| Task.FromResult(new ProcessResult(0, string.Empty, string.Empty)); | ||
| } | ||
| } |
27 changes: 27 additions & 0 deletions
27
src/GitAutomation.Tests/Microsoft.DotNet.GitAutomation.Tests.csproj
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,27 @@ | ||
| <Project Sdk="Microsoft.NET.Sdk"> | ||
|
|
||
| <PropertyGroup> | ||
| <TargetFramework>net9.0</TargetFramework> | ||
| <IsPackable>false</IsPackable> | ||
| <SuppressNETCoreSdkPreviewMessage>true</SuppressNETCoreSdkPreviewMessage> | ||
| <TreatWarningsAsErrors>true</TreatWarningsAsErrors> | ||
| <Nullable>enable</Nullable> | ||
| <ImplicitUsings>enable</ImplicitUsings> | ||
| </PropertyGroup> | ||
|
|
||
| <PropertyGroup> | ||
| <IsTestProject>true</IsTestProject> | ||
| <SignAssembly>false</SignAssembly> | ||
| <TestRunnerName>MSTest</TestRunnerName> | ||
| </PropertyGroup> | ||
|
|
||
| <ItemGroup> | ||
| <PackageReference Include="CsCheck" Version="4.6.0" /> | ||
| <PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="9.0.17" /> | ||
| </ItemGroup> | ||
|
|
||
| <ItemGroup> | ||
| <ProjectReference Include="../GitAutomation/Microsoft.DotNet.GitAutomation.csproj" /> | ||
| </ItemGroup> | ||
|
|
||
| </Project> |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,217 @@ | ||
| // Licensed to the .NET Foundation under one or more agreements. | ||
| // The .NET Foundation licenses this file to you under the MIT license. | ||
| // See the LICENSE file in the project root for more information. | ||
|
|
||
| using CsCheck; | ||
|
|
||
| namespace Microsoft.DotNet.GitAutomation.Tests; | ||
|
|
||
| [TestClass] | ||
| public sealed class PullRequestPlannerTests | ||
| { | ||
| private const string Workspace = "test-workspace"; | ||
|
|
||
| private static readonly AutomationIdentity AutomationIdentity = new("bot", "bot@example.com"); | ||
|
|
||
| private static readonly Uri Url = new("https://github.com/dotnet/example/pull/1"); | ||
|
|
||
| // A git object SHA-1: a 40-character lowercase hex hash (tree hash, commit SHA, etc.). | ||
| private static readonly Gen<string> GenSha1 = Gen.String[Gen.Char["0123456789abcdef"], 40, 40]; | ||
|
|
||
| private static readonly Gen<int> GenPullRequestNumber = Gen.Int[1, 99999]; | ||
|
|
||
| private static readonly Gen<PullRequestUpdateStrategy> GenUpdateStrategy = | ||
| Gen.OneOfConst( | ||
| PullRequestUpdateStrategy.Append, | ||
| PullRequestUpdateStrategy.Replace); | ||
|
|
||
| private static readonly Gen<ForeignCommitPolicy> GenForeignCommitPolicy = | ||
| Gen.OneOfConst( | ||
| ForeignCommitPolicy.Proceed, | ||
| ForeignCommitPolicy.Stop); | ||
|
|
||
| private static readonly Gen<PullRequestState> GenPullRequestState = | ||
| from key in Gen.OneOfConst("product-a", "product-b") | ||
|
github-code-quality[bot] marked this conversation as resolved.
Fixed
lbussell marked this conversation as resolved.
Dismissed
|
||
| from title in Gen.OneOfConst("Title A", "Title B") | ||
|
github-code-quality[bot] marked this conversation as resolved.
Fixed
github-code-quality[bot] marked this conversation as resolved.
Fixed
lbussell marked this conversation as resolved.
Dismissed
|
||
| from body in Gen.OneOfConst("", "Body A", "Body B") | ||
|
github-code-quality[bot] marked this conversation as resolved.
Fixed
github-code-quality[bot] marked this conversation as resolved.
Fixed
lbussell marked this conversation as resolved.
Dismissed
|
||
| from targetBranch in Gen.OneOfConst("main", "nightly", "release", "release/1.0") | ||
|
github-code-quality[bot] marked this conversation as resolved.
Fixed
github-code-quality[bot] marked this conversation as resolved.
Fixed
lbussell marked this conversation as resolved.
Dismissed
|
||
| from treeHash in GenSha1 | ||
|
github-code-quality[bot] marked this conversation as resolved.
Fixed
github-code-quality[bot] marked this conversation as resolved.
Fixed
lbussell marked this conversation as resolved.
Dismissed
|
||
| select new PullRequestState(key, title, body, targetBranch, treeHash); | ||
|
|
||
| private static readonly Gen<TargetBranchState> GenTargetBranchState = | ||
| GenSha1.Select(treeHash => new TargetBranchState(treeHash)); | ||
|
|
||
| private static readonly Gen<CommitInfo> GenAutomationCommit = | ||
| GenSha1.Select(sha => new CommitInfo(sha, AutomationIdentity.AuthorName, AutomationIdentity.AuthorEmail)); | ||
|
|
||
| private static readonly Gen<CommitInfo> GenForeignCommit = | ||
| GenSha1.Select(sha => new CommitInfo(sha, "Person", "person@example.com")); | ||
|
|
||
| private static readonly Gen<CommitInfo> GenRandomCommit = Gen.Frequency((3, GenAutomationCommit), (1, GenForeignCommit)); | ||
|
|
||
| private static readonly Gen<CommitInfo[]> GenRandomCommits = GenRandomCommit.Array[1, 3]; | ||
|
|
||
| private static Gen<CommitInfo[]> GenCommitsGuaranteedForeign = | ||
| from foreign in GenForeignCommit | ||
|
github-code-quality[bot] marked this conversation as resolved.
Fixed
github-code-quality[bot] marked this conversation as resolved.
Fixed
lbussell marked this conversation as resolved.
Dismissed
|
||
| from rest in GenRandomCommit.Array[0, 2] | ||
|
github-code-quality[bot] marked this conversation as resolved.
Fixed
github-code-quality[bot] marked this conversation as resolved.
Fixed
lbussell marked this conversation as resolved.
Dismissed
|
||
| from commits in Gen.Shuffle((CommitInfo[])[foreign, .. rest]) | ||
|
github-code-quality[bot] marked this conversation as resolved.
Fixed
github-code-quality[bot] marked this conversation as resolved.
Fixed
lbussell marked this conversation as resolved.
Dismissed
|
||
| select commits; | ||
|
|
||
| // An arbitrary existing pull request, whose branch may include foreign commits. | ||
| private static readonly Gen<ExistingPullRequest> GenExistingPullRequest = | ||
| from content in GenPullRequestState | ||
|
github-code-quality[bot] marked this conversation as resolved.
Fixed
github-code-quality[bot] marked this conversation as resolved.
Fixed
lbussell marked this conversation as resolved.
Dismissed
|
||
| from number in GenPullRequestNumber | ||
|
github-code-quality[bot] marked this conversation as resolved.
Fixed
github-code-quality[bot] marked this conversation as resolved.
Fixed
lbussell marked this conversation as resolved.
Dismissed
|
||
| from commits in GenRandomCommits | ||
| select new ExistingPullRequest(content, number, Url, commits); | ||
|
|
||
| // Bundles the planner inputs for a single test case. | ||
| private sealed record PullRequestScenario( | ||
| PullRequestState DesiredState, | ||
| TargetBranchState TargetBranch, | ||
| ExistingPullRequest? ExistingPullRequest, | ||
| PullRequestUpdateStrategy UpdateStrategy, | ||
| ForeignCommitPolicy OnForeignCommits) | ||
| { | ||
| public IEnumerable<IOperation> Plan() => | ||
| Planner.Plan( | ||
| workspaceDirectory: Workspace, | ||
| identity: AutomationIdentity, | ||
| desiredState: DesiredState, | ||
| targetBranch: TargetBranch, | ||
| existingPullRequest: ExistingPullRequest, | ||
| updateStrategy: UpdateStrategy, | ||
| onForeignCommits: OnForeignCommits); | ||
| }; | ||
|
|
||
| // If no PR exists, and there are changes to be made, then a new PR is | ||
| // always created. | ||
| [TestMethod] | ||
| public void ChangesWithNoPR_CreatesNewPR() | ||
| { | ||
| var scenario = | ||
| from desiredState in GenPullRequestState | ||
| from targetBranch in GenTargetBranchState | ||
| from updateStrategy in GenUpdateStrategy | ||
| from onForeignCommits in GenForeignCommitPolicy | ||
| // Collision should be very unlikely, but filter it out anyways | ||
| where desiredState.TreeHash != targetBranch.TreeHash | ||
| select new PullRequestScenario( | ||
| DesiredState: desiredState, | ||
| TargetBranch: targetBranch, | ||
| ExistingPullRequest: null, | ||
| UpdateStrategy: updateStrategy, | ||
| OnForeignCommits: onForeignCommits); | ||
|
|
||
| scenario.Sample(s => s.Plan().OfType<CreatePullRequestOperation>().Count() == 1); | ||
| } | ||
|
|
||
| // If no PR exists, and there are changes to be made, the source branch is | ||
| // always force pushed to reset it to the state of the target branch. | ||
| [TestMethod] | ||
| public void ChangesWithNoPR_ResetsExistingBranch() | ||
| { | ||
| var scenario = | ||
| from desiredState in GenPullRequestState | ||
| from targetBranch in GenTargetBranchState | ||
| from updateStrategy in GenUpdateStrategy | ||
| from onForeignCommits in GenForeignCommitPolicy | ||
| // Collision should be very unlikely, but filter it out anyways | ||
| where desiredState.TreeHash != targetBranch.TreeHash | ||
| select new PullRequestScenario( | ||
| DesiredState: desiredState, | ||
| TargetBranch: targetBranch, | ||
| ExistingPullRequest: null, | ||
| UpdateStrategy: updateStrategy, | ||
| OnForeignCommits: onForeignCommits); | ||
|
|
||
| scenario.Sample(s => s.Plan().OfType<PushCommitsOperation>().Single().ForcePush); | ||
| } | ||
|
|
||
| // For all scenarios where the desired tree is already present in an | ||
| // existing pull request, no actions are taken. | ||
| [TestMethod] | ||
| public void NoChanges_NoOp() | ||
| { | ||
| var scenario = | ||
| from desiredState in GenPullRequestState | ||
| from prNumber in GenPullRequestNumber | ||
| from existingCommits in GenRandomCommits | ||
| from targetBranch in GenTargetBranchState | ||
| from updateStrategy in GenUpdateStrategy | ||
| select new PullRequestScenario( | ||
| desiredState, | ||
| targetBranch, | ||
| new ExistingPullRequest(desiredState, prNumber, Url, existingCommits), | ||
| updateStrategy, | ||
| // Don't block on foreign commits | ||
| ForeignCommitPolicy.Proceed); | ||
|
|
||
| scenario.Sample(s => !s.Plan().Any()); | ||
| } | ||
|
|
||
| // For all scenarios where the desired tree already equals the target tree, | ||
| // nothing is pushed. | ||
| [TestMethod] | ||
| public void NoChanges_DoesNotPush() | ||
| { | ||
| var scenario = | ||
| from desiredState in GenPullRequestState | ||
| // 20% of scenarios will have no existing pull request | ||
| from existingPullRequest in Gen.Null(GenExistingPullRequest, 0.2) | ||
| from targetTree in GenSha1 | ||
| from updateStrategy in GenUpdateStrategy | ||
| // The base tree is the existing PR's head, or the target branch when none exists. | ||
| // Pin the desired tree to it so there is no content diff to push. | ||
| select new PullRequestScenario( | ||
| desiredState with { TreeHash = existingPullRequest?.Content.TreeHash ?? targetTree }, | ||
| new TargetBranchState(targetTree), | ||
| existingPullRequest, | ||
| updateStrategy, | ||
| // Don't block on foreign commits | ||
| ForeignCommitPolicy.Proceed); | ||
|
|
||
| scenario.Sample(s => !s.Plan().OfType<PushCommitsOperation>().Any()); | ||
| } | ||
|
|
||
| // If there is already an open PR, never decide to create a second one. | ||
| [TestMethod] | ||
| public void ExistingPR_DoesNotCreateNewPR() | ||
| { | ||
| var scenario = | ||
| from desiredState in GenPullRequestState | ||
| from existingPullRequest in GenExistingPullRequest | ||
| from targetBranch in GenTargetBranchState | ||
| from updateStrategy in GenUpdateStrategy | ||
| from onForeignCommits in GenForeignCommitPolicy | ||
| select new PullRequestScenario( | ||
| desiredState, | ||
| targetBranch, | ||
| existingPullRequest, | ||
| updateStrategy, | ||
| onForeignCommits); | ||
|
|
||
| scenario.Sample(s => !s.Plan().OfType<CreatePullRequestOperation>().Any()); | ||
| } | ||
|
|
||
| // For all scenarios where ForeignCommitPolicy.Stop is set, and there is a foreign commit on | ||
| // the PR branch, no action should be taken. | ||
| [TestMethod] | ||
| public void ExistingPR_StopsOnForeignCommits() | ||
| { | ||
| var scenario = | ||
| from desiredState in GenPullRequestState | ||
| from existingState in GenPullRequestState | ||
| from prNumber in GenPullRequestNumber | ||
| from commits in GenCommitsGuaranteedForeign | ||
| from targetBranch in GenTargetBranchState | ||
| from updateStrategy in GenUpdateStrategy | ||
| select new PullRequestScenario( | ||
| desiredState, | ||
| targetBranch, | ||
| new ExistingPullRequest(existingState, prNumber, Url, commits), | ||
| updateStrategy, | ||
| OnForeignCommits: ForeignCommitPolicy.Stop); | ||
|
|
||
| scenario.Sample(s => !s.Plan().Any()); | ||
| } | ||
| } | ||
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.