public async Task Given_Parameters_When_MergePrAsync_Invoked_Then_It_Should_Return_Property() { var sha = this._fixture.Create <string>(); var merged = this._fixture.Create <bool>(); var prm = new PullRequestMerge().SetValue("Merged", merged); var prc = new Mock <IPullRequestsClient>(); prc.Setup(p => p.Merge(It.IsAny <string>(), It.IsAny <string>(), It.IsAny <int>(), It.IsAny <MergePullRequest>())).ReturnsAsync(prm); var client = new Mock <IGitHubClient>(); client.SetupGet(p => p.PullRequest).Returns(prc.Object); var options = new Options(); var handler = new MessageHandler() .SetValue("Sha", sha) .WithGitHubClient(client.Object); await handler.MergePrAsync(options).ConfigureAwait(false); handler.IsMerged.Should().Be(prm.Merged); }
/* * This tool is designed to execute most of the git operations required when releasing: * 1. Merge the RC PR into the release branch. * 2. Draft a GitHub release using the changelog notes. * 3. Open a PR from the release-branch into source-branch. */ public int Run() { Common.VerifySemanticVersioningFormat(options.Version); var(repoName, pullRequestId) = ExtractPullRequestInfo(options.PullRequestUrl); var gitHubClient = new GitHubClient(options); var repoUrl = string.Format(Common.RepoUrlTemplate, options.GithubOrgName, repoName); var gitHubRepo = gitHubClient.GetRepositoryFromUrl(repoUrl); // Check if the PR has been merged already. // If it has, log the PR URL and move on. // This ensures the idempotence of the pipeline. if (gitHubClient.GetMergeState(gitHubRepo, pullRequestId) == GitHubClient.MergeState.AlreadyMerged) { Logger.Info("Candidate branch has already merged into release branch. No merge operation will be attempted."); // Check if a PR has already been opened from release branch into source branch. // If it has, log the PR URL and move on. // This ensures the idempotence of the pipeline. var githubOrg = options.GithubOrgName; var branchFrom = $"{options.CandidateBranch}-cleanup"; var branchTo = options.SourceBranch; if (!gitHubClient.TryGetPullRequest(gitHubRepo, githubOrg, branchFrom, branchTo, out var pullRequest)) { try { using (var gitClient = GitClient.FromRemote(repoUrl)) { gitClient.CheckoutRemoteBranch(options.ReleaseBranch); gitClient.ForcePush(branchFrom); } pullRequest = gitHubClient.CreatePullRequest(gitHubRepo, branchFrom, branchTo, string.Format(PullRequestNameTemplate, options.Version, options.ReleaseBranch, options.SourceBranch), string.Format(pullRequestBody, options.ReleaseBranch, options.SourceBranch)); } catch (Octokit.ApiValidationException e) { // Handles the case where source-branch (default master) and release-branch (default release) are identical, so there is no need to merge source-branch back into release-branch. if (e.ApiError.Errors.Count > 0 && e.ApiError.Errors[0].Message.Contains("No commits between")) { Logger.Info(e.ApiError.Errors[0].Message); Logger.Info("No PR will be created."); return(0); } throw; } } else { Logger.Info("A PR has already been opened from release branch into source branch: {0}", pullRequest.HtmlUrl); } var prAnnotation = string.Format(prAnnotationTemplate, pullRequest.HtmlUrl, repoName, options.ReleaseBranch, options.SourceBranch); BuildkiteAgent.Annotate(AnnotationLevel.Info, "release-into-source-prs", prAnnotation, true); Logger.Info("Pull request available: {0}", pullRequest.HtmlUrl); Logger.Info("Successfully created PR from release branch into source branch."); Logger.Info("Merge hash: {0}", pullRequest.MergeCommitSha); return(0); } var remoteUrl = string.Format(Common.RepoUrlTemplate, options.GithubOrgName, repoName); try { // 1. Clones the source repo. using (var gitClient = GitClient.FromRemote(remoteUrl)) { // 2. Checks out the candidate branch, which defaults to 4.xx-SpatialOSUnrealGDK-x.y.z-rc in UnrealEngine and x.y.z-rc in all other repos. gitClient.CheckoutRemoteBranch(options.CandidateBranch); // 3. Makes repo-specific changes for prepping the release (e.g. updating version files, formatting the CHANGELOG). switch (repoName) { case "UnrealEngine": UpdateVersionFile(gitClient, options.Version, UnrealGDKVersionFile); UpdateVersionFile(gitClient, options.Version, UnrealGDKExampleProjectVersionFile); break; case "UnrealGDK": UpdateChangeLog(ChangeLogFilename, options, gitClient); var releaseHashes = options.EngineVersions.Split(" ") .Select(version => $"{version.Trim()}-release") .Select(BuildkiteAgent.GetMetadata) .Select(hash => $"UnrealEngine-{hash}") .ToList(); UpdateUnrealEngineVersionFile(releaseHashes, gitClient); break; case "UnrealGDKExampleProject": UpdateVersionFile(gitClient, options.Version, UnrealGDKVersionFile); break; case "UnrealGDKTestGyms": UpdateVersionFile(gitClient, options.Version, UnrealGDKVersionFile); break; case "UnrealGDKEngineNetTest": UpdateVersionFile(gitClient, options.Version, UnrealGDKVersionFile); break; case "TestGymBuildKite": UpdateVersionFile(gitClient, options.Version, UnrealGDKVersionFile); break; } // 4. Commit changes and push them to a remote candidate branch. gitClient.Commit(string.Format(CandidateCommitMessageTemplate, options.Version)); gitClient.ForcePush(options.CandidateBranch); } // Since we've pushed changes, we need to wait for all checks to pass before attempting to merge it. var startTime = DateTime.Now; while (true) { if (DateTime.Now.Subtract(startTime) > TimeSpan.FromHours(12)) { throw new Exception($"Exceeded timeout waiting for PR to be mergeable: {options.PullRequestUrl}"); } if (gitHubClient.GetMergeState(gitHubRepo, pullRequestId) == GitHubClient.MergeState.ReadyToMerge) { Logger.Info($"{options.PullRequestUrl} is mergeable. Attempting to merge."); break; } Logger.Info($"{options.PullRequestUrl} is not in a mergeable state, will query mergeability again in one minute."); Thread.Sleep(TimeSpan.FromMinutes(1)); } PullRequestMerge mergeResult = null; while (true) { // Merge into release try { mergeResult = gitHubClient.MergePullRequest(gitHubRepo, pullRequestId, PullRequestMergeMethod.Merge); } catch (Octokit.PullRequestNotMergeableException e) {} // Will be covered by log below if (DateTime.Now.Subtract(startTime) > TimeSpan.FromHours(12)) { throw new Exception($"Exceeded timeout waiting for PR to be mergeable: {options.PullRequestUrl}"); } if (!mergeResult.Merged) { Logger.Info($"Was unable to merge pull request at: {options.PullRequestUrl}. Received error: {mergeResult.Message}"); Logger.Info($"{options.PullRequestUrl} is not in a mergeable state, will query mergeability again in one minute."); Thread.Sleep(TimeSpan.FromMinutes(1)); } else { break; } } Logger.Info($"{options.PullRequestUrl} had been merged."); // This uploads the commit hashes of the merge into release. // When run against UnrealGDK, the UnrealEngine hashes are used to update the unreal-engine.version file to include the UnrealEngine release commits. BuildkiteAgent.SetMetaData(options.ReleaseBranch, mergeResult.Sha); //TODO: UNR-3615 - Fix this so it does not throw Octokit.ApiValidationException: Reference does not exist. // Delete candidate branch. //gitHubClient.DeleteBranch(gitHubClient.GetRepositoryFromUrl(repoUrl), options.CandidateBranch); using (var gitClient = GitClient.FromRemote(repoUrl)) { // Create GitHub release in the repo gitClient.Fetch(); gitClient.CheckoutRemoteBranch(options.ReleaseBranch); var release = CreateRelease(gitHubClient, gitHubRepo, gitClient, repoName); BuildkiteAgent.Annotate(AnnotationLevel.Info, "draft-releases", string.Format(releaseAnnotationTemplate, release.HtmlUrl, repoName), true); Logger.Info("Release Successful!"); Logger.Info("Release hash: {0}", gitClient.GetHeadCommit().Sha); Logger.Info("Draft release: {0}", release.HtmlUrl); } // Check if a PR has already been opened from release branch into source branch. // If it has, log the PR URL and move on. // This ensures the idempotence of the pipeline. var githubOrg = options.GithubOrgName; var branchFrom = $"{options.CandidateBranch}-cleanup"; var branchTo = options.SourceBranch; if (!gitHubClient.TryGetPullRequest(gitHubRepo, githubOrg, branchFrom, branchTo, out var pullRequest)) { try { using (var gitClient = GitClient.FromRemote(repoUrl)) { gitClient.CheckoutRemoteBranch(options.ReleaseBranch); gitClient.ForcePush(branchFrom); } pullRequest = gitHubClient.CreatePullRequest(gitHubRepo, branchFrom, branchTo, string.Format(PullRequestNameTemplate, options.Version, options.ReleaseBranch, options.SourceBranch), string.Format(pullRequestBody, options.ReleaseBranch, options.SourceBranch)); } catch (Octokit.ApiValidationException e) { // Handles the case where source-branch (default master) and release-branch (default release) are identical, so there is no need to merge source-branch back into release-branch. if (e.ApiError.Errors.Count > 0 && e.ApiError.Errors[0].Message.Contains("No commits between")) { Logger.Info(e.ApiError.Errors[0].Message); Logger.Info("No PR will be created."); return(0); } throw; } } else { Logger.Info("A PR has already been opened from release branch into source branch: {0}", pullRequest.HtmlUrl); } var prAnnotation = string.Format(prAnnotationTemplate, pullRequest.HtmlUrl, repoName, options.ReleaseBranch, options.SourceBranch); BuildkiteAgent.Annotate(AnnotationLevel.Info, "release-into-source-prs", prAnnotation, true); Logger.Info("Pull request available: {0}", pullRequest.HtmlUrl); Logger.Info($"Successfully created PR for merging {options.ReleaseBranch} into {options.SourceBranch}."); } catch (Exception e) { Logger.Error(e, $"ERROR: Unable to merge {options.CandidateBranch} into {options.ReleaseBranch} and/or clean up by merging {options.ReleaseBranch} into {options.SourceBranch}. Error: {0}", e); return(1); } return(0); }
/* * This tool is designed to execute most of the git operations required when releasing: * 1. Merge the RC PR into the release branch. * 2. Draft a GitHub release using the changelog notes. * 3. Open a PR from the release-branch into source-branch. */ public int Run() { Common.VerifySemanticVersioningFormat(options.Version); var gitRepoName = options.GitRepoName; var repoUrl = Common.makeRepoUrl(options.GithubOrgName, gitRepoName); var gitHubClient = new GitHubClient(options); var gitHubRepo = gitHubClient.GetRepositoryFromUrl(repoUrl); if (string.IsNullOrWhiteSpace(options.PullRequestUrl.Trim().Replace("\"", ""))) { Logger.Info("The passed PullRequestUrl was empty or missing. Trying to release without merging a PR."); using (var gitClient = GitClient.FromRemote(repoUrl)) { // Create the release branch, since if there is no PR, the release branch did not exist previously gitClient.Fetch(); if (gitClient.LocalBranchExists($"origin/{options.ReleaseBranch}")) { Logger.Error("The PullRequestUrl was empty or missing, but the release branch already exists, so presuming this step already ran."); } else { gitClient.CheckoutRemoteBranch(options.CandidateBranch); gitClient.ForcePush(options.ReleaseBranch); } gitClient.Fetch(); gitClient.CheckoutRemoteBranch(options.ReleaseBranch); FinalizeRelease(gitHubClient, gitClient, gitHubRepo, gitRepoName, repoUrl); } return(0); } var(repoName, pullRequestId) = Common.ExtractPullRequestInfo(options.PullRequestUrl); if (gitRepoName != repoName) { Logger.Error($"Repository names given do not match. Repository name given: {gitRepoName}, PR URL repository name: {repoName}."); return(1); } // Check if the PR has been merged already. // If it has, log the PR URL and move on. // This ensures the idempotence of the pipeline. if (gitHubClient.GetMergeState(gitHubRepo, pullRequestId) == GitHubClient.MergeState.AlreadyMerged) { Logger.Info("Candidate branch has already merged into release branch. No merge operation will be attempted."); // null for GitClient will let it create one if necessary CreatePRFromReleaseToSource(gitHubClient, gitHubRepo, repoUrl, repoName, null); return(0); } var remoteUrl = Common.makeRepoUrl(options.GithubOrgName, repoName); try { // Only do something for the UnrealGDK, since the other repos should have been prepped by the PrepFullReleaseCommand. if (repoName == "UnrealGDK") { // 1. Clones the source repo. using (var gitClient = GitClient.FromRemote(remoteUrl)) { // 2. Checks out the candidate branch, which defaults to 4.xx-SpatialOSUnrealGDK-x.y.z-rc in UnrealEngine and x.y.z-rc in all other repos. gitClient.CheckoutRemoteBranch(options.CandidateBranch); // 3. Makes repo-specific changes for prepping the release (e.g. updating version files, formatting the CHANGELOG). Common.UpdateChangeLog(gitClient, options.Version); var releaseHashes = options.EngineVersions.Replace("\"", "").Split(" ") .Select(version => $"{version.Trim()}") .Select(BuildkiteAgent.GetMetadata) .Select(hash => $"{hash}") .ToList(); Common.UpdateUnrealEngineVersionFile(gitClient, releaseHashes); // 4. Commit changes and push them to a remote candidate branch. gitClient.Commit(string.Format(CandidateCommitMessageTemplate, options.Version)); gitClient.ForcePush(options.CandidateBranch); } } // Since we've (maybe) pushed changes, we need to wait for all checks to pass before attempting to merge it. var startTime = DateTime.Now; while (true) { if (DateTime.Now.Subtract(startTime) > TimeSpan.FromHours(12)) { throw new Exception($"Exceeded timeout waiting for PR to be mergeable: {options.PullRequestUrl}"); } if (gitHubClient.GetMergeState(gitHubRepo, pullRequestId) == GitHubClient.MergeState.ReadyToMerge) { Logger.Info($"{options.PullRequestUrl} is mergeable. Attempting to merge."); break; } Logger.Info($"{options.PullRequestUrl} is not in a mergeable state, will query mergeability again in one minute."); Thread.Sleep(TimeSpan.FromMinutes(1)); } PullRequestMerge mergeResult = null; while (true) { // Merge into release try { mergeResult = gitHubClient.MergePullRequest(gitHubRepo, pullRequestId, PullRequestMergeMethod.Merge, $"Merging final GDK for Unreal {options.Version} release"); } catch (Octokit.PullRequestNotMergeableException e) { Logger.Info($"Was unable to merge pull request at: {options.PullRequestUrl}. Received error: {e.Message}"); } if (DateTime.Now.Subtract(startTime) > TimeSpan.FromHours(12)) { throw new Exception($"Exceeded timeout waiting for PR to be mergeable: {options.PullRequestUrl}"); } if (!mergeResult.Merged) { Logger.Info($"{options.PullRequestUrl} is not in a mergeable state, will query mergeability again in one minute."); Thread.Sleep(TimeSpan.FromMinutes(1)); } else { break; } } Logger.Info($"{options.PullRequestUrl} had been merged."); using (var gitClient = GitClient.FromRemote(repoUrl)) { // Create GitHub release in the repo gitClient.Fetch(); gitClient.CheckoutRemoteBranch(options.ReleaseBranch); FinalizeRelease(gitHubClient, gitClient, gitHubRepo, gitRepoName, repoUrl); } } catch (Exception e) { Logger.Error(e, $"ERROR: Unable to merge {options.CandidateBranch} into {options.ReleaseBranch} and/or clean up by merging {options.ReleaseBranch} into {options.SourceBranch}. Error: {e.Message}"); return(1); } return(0); }
/* * This tool is designed to execute most of the git operations required when releasing: * 1. Merge the RC PR into the release branch. * 2. Draft a GitHub release using the changelog notes. * 3. Open a PR from the release-branch into source-branch. */ public int Run() { Common.VerifySemanticVersioningFormat(options.Version); var(repoName, pullRequestId) = Common.ExtractPullRequestInfo(options.PullRequestUrl); var gitHubClient = new GitHubClient(options); var repoUrl = string.Format(Common.RepoUrlTemplate, options.GithubOrgName, repoName); var gitHubRepo = gitHubClient.GetRepositoryFromUrl(repoUrl); // Check if the PR has been merged already. // If it has, log the PR URL and move on. // This ensures the idempotence of the pipeline. if (gitHubClient.GetMergeState(gitHubRepo, pullRequestId) == GitHubClient.MergeState.AlreadyMerged) { Logger.Info("Candidate branch has already merged into release branch. No merge operation will be attempted."); // null for GitClient will let it create one if necessary CreatePRFromReleaseToSource(gitHubClient, gitHubRepo, repoUrl, repoName, null); return(0); } var remoteUrl = string.Format(Common.RepoUrlTemplate, options.GithubOrgName, repoName); try { // Only do something for the UnrealGDK, since the other repos should have been prepped by the PrepFullReleaseCommand. if (repoName == "UnrealGDK") { // 1. Clones the source repo. using (var gitClient = GitClient.FromRemote(remoteUrl)) { // 2. Checks out the candidate branch, which defaults to 4.xx-SpatialOSUnrealGDK-x.y.z-rc in UnrealEngine and x.y.z-rc in all other repos. gitClient.CheckoutRemoteBranch(options.CandidateBranch); // 3. Makes repo-specific changes for prepping the release (e.g. updating version files, formatting the CHANGELOG). Common.UpdateChangeLog(ChangeLogFilename, options.Version, gitClient, ChangeLogReleaseHeadingTemplate); var releaseHashes = options.EngineVersions.Split(" ") .Select(version => $"{version.Trim()}-release") .Select(BuildkiteAgent.GetMetadata) .Select(hash => $"UnrealEngine-{hash}") .ToList(); UpdateUnrealEngineVersionFile(releaseHashes, gitClient); // 4. Commit changes and push them to a remote candidate branch. gitClient.Commit(string.Format(CandidateCommitMessageTemplate, options.Version)); gitClient.ForcePush(options.CandidateBranch); } } // Since we've (maybe) pushed changes, we need to wait for all checks to pass before attempting to merge it. var startTime = DateTime.Now; while (true) { if (DateTime.Now.Subtract(startTime) > TimeSpan.FromHours(12)) { throw new Exception($"Exceeded timeout waiting for PR to be mergeable: {options.PullRequestUrl}"); } if (gitHubClient.GetMergeState(gitHubRepo, pullRequestId) == GitHubClient.MergeState.ReadyToMerge) { Logger.Info($"{options.PullRequestUrl} is mergeable. Attempting to merge."); break; } Logger.Info($"{options.PullRequestUrl} is not in a mergeable state, will query mergeability again in one minute."); Thread.Sleep(TimeSpan.FromMinutes(1)); } PullRequestMerge mergeResult = null; while (true) { // Merge into release try { mergeResult = gitHubClient.MergePullRequest(gitHubRepo, pullRequestId, PullRequestMergeMethod.Merge, $"Merging final GDK for Unreal {options.Version} release"); } catch (Octokit.PullRequestNotMergeableException e) { Logger.Info($"Was unable to merge pull request at: {options.PullRequestUrl}. Received error: {e.Message}"); } if (DateTime.Now.Subtract(startTime) > TimeSpan.FromHours(12)) { throw new Exception($"Exceeded timeout waiting for PR to be mergeable: {options.PullRequestUrl}"); } if (!mergeResult.Merged) { Logger.Info($"{options.PullRequestUrl} is not in a mergeable state, will query mergeability again in one minute."); Thread.Sleep(TimeSpan.FromMinutes(1)); } else { break; } } Logger.Info($"{options.PullRequestUrl} had been merged."); // This uploads the commit hashes of the merge into release. // When run against UnrealGDK, the UnrealEngine hashes are used to update the unreal-engine.version file to include the UnrealEngine release commits. BuildkiteAgent.SetMetaData(options.ReleaseBranch, mergeResult.Sha); // TODO: UNR-3615 - Fix this so it does not throw Octokit.ApiValidationException: Reference does not exist. // Delete candidate branch. //gitHubClient.DeleteBranch(gitHubClient.GetRepositoryFromUrl(repoUrl), options.CandidateBranch); using (var gitClient = GitClient.FromRemote(repoUrl)) { // Create GitHub release in the repo gitClient.Fetch(); gitClient.CheckoutRemoteBranch(options.ReleaseBranch); var release = CreateRelease(gitHubClient, gitHubRepo, gitClient, repoName); BuildkiteAgent.Annotate(AnnotationLevel.Info, "draft-releases", string.Format(releaseAnnotationTemplate, release.HtmlUrl, repoName), true); Logger.Info("Release Successful!"); Logger.Info("Release hash: {0}", gitClient.GetHeadCommit().Sha); Logger.Info("Draft release: {0}", release.HtmlUrl); CreatePRFromReleaseToSource(gitHubClient, gitHubRepo, repoUrl, repoName, gitClient); } } catch (Exception e) { Logger.Error(e, $"ERROR: Unable to merge {options.CandidateBranch} into {options.ReleaseBranch} and/or clean up by merging {options.ReleaseBranch} into {options.SourceBranch}. Error: {0}", e.Message); return(1); } return(0); }