Skip to content

Commit

Permalink
Add Github "enable auto merge" PR option
Browse files Browse the repository at this point in the history
This is using the GraphQL API since it is not available in the Java client.
  • Loading branch information
ja-openai committed Sep 17, 2024
1 parent b17489e commit be02edf
Show file tree
Hide file tree
Showing 3 changed files with 127 additions and 23 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import com.beust.jcommander.Parameter;
import com.beust.jcommander.Parameters;
import com.box.l10n.mojito.cli.console.ConsoleWriter;
import com.box.l10n.mojito.github.GithubClient;
import com.box.l10n.mojito.github.GithubClients;
import com.box.l10n.mojito.github.GithubException;
import java.util.List;
Expand Down Expand Up @@ -75,6 +76,19 @@ public class GithubCreatePRCommand extends Command {
description = "The PR reviewers")
List<String> reviewers;

@Parameter(
names = {"--enable-auto-merge"},
required = false,
arity = 1,
description = "Enable auto-merge with the specified method")
EnableAutoMergeType enableAutoMerge = EnableAutoMergeType.NONE;

enum EnableAutoMergeType {
SQUASH,
MERGE,
NONE
}

@Override
public boolean shouldShowInCommandList() {
return false;
Expand All @@ -86,8 +100,10 @@ protected void execute() throws CommandException {

GHPullRequest pr =
githubClients.getClient(owner).createPR(repository, title, head, base, body, reviewers);

consoleWriter.a("PR created: ").fg(Ansi.Color.CYAN).a(pr.getHtmlUrl().toString()).println();
if (!EnableAutoMergeType.NONE.equals(enableAutoMerge)) {
githubClients.getClient(owner).enableAutoMerge(pr, GithubClient.AutoMergeType.SQUASH);
}
} catch (GithubException e) {
throw new CommandException(e);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,6 @@
import com.beust.jcommander.Parameters;
import com.box.l10n.mojito.cli.console.ConsoleWriter;
import com.box.l10n.mojito.github.GithubClients;
import java.io.IOException;
import java.security.NoSuchAlgorithmException;
import java.security.spec.InvalidKeySpecException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.Scope;
Expand Down Expand Up @@ -50,7 +47,7 @@ protected void execute() throws CommandException {
consoleWriter
.a(githubClients.getClient(owner).getGithubAppInstallationToken(repository).getToken())
.print();
} catch (IOException | NoSuchAlgorithmException | InvalidKeySpecException e) {
} catch (Exception e) {
throw new CommandException(e);
}
}
Expand Down
127 changes: 109 additions & 18 deletions common/src/main/java/com/box/l10n/mojito/github/GithubClient.java
Original file line number Diff line number Diff line change
@@ -1,16 +1,24 @@
package com.box.l10n.mojito.github;

import com.box.l10n.mojito.json.ObjectMapper;
import com.fasterxml.jackson.core.type.TypeReference;
import com.google.common.collect.ImmutableMap;
import io.jsonwebtoken.JwtBuilder;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import java.io.IOException;
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.security.KeyFactory;
import java.security.NoSuchAlgorithmException;
import java.security.PrivateKey;
import java.security.spec.InvalidKeySpecException;
import java.security.spec.PKCS8EncodedKeySpec;
import java.util.Date;
import java.util.List;
import java.util.Map;
import org.apache.commons.codec.binary.Base64;
import org.kohsuke.github.GHAppInstallationToken;
import org.kohsuke.github.GHCommitState;
Expand Down Expand Up @@ -306,6 +314,15 @@ public List<GHIssueComment> getPRComments(String repository, int prNumber) {
}
}

public void listPR(String repository) {
try {
GitHub gc = getGithubClient(repository);
gc.getRepository(repository);
} catch (IOException | NoSuchAlgorithmException | InvalidKeySpecException e) {
throw new RuntimeException(e);
}
}

public GHPullRequest createPR(
String repository,
String title,
Expand Down Expand Up @@ -335,16 +352,87 @@ public GHPullRequest createPR(

pullRequest.requestReviewers(reviewersGH);
}

return pullRequest;
} catch (IOException | NoSuchAlgorithmException | InvalidKeySpecException e) {
} catch (Exception e) {
String message =
String.format("Error creating a PR in repository '%s': %s", repoFullPath, e.getMessage());
logger.error(message, e);
throw new GithubException(message, e);
}
}

public enum AutoMergeType {
SQUASH,
MERGE
}

/**
* This is implemented using the GraphQL API since, the Java client does not support this method
* yet.
*/
public void enableAutoMerge(GHPullRequest pullRequest, AutoMergeType autoMergeType) {

logger.debug(
"Enable Auto Merge on PR: {}, with type: {}",
pullRequest.getNumber(),
autoMergeType.name());

ObjectMapper objectMapper = new ObjectMapper();

HttpResponse<String> mutationResponse;

try (HttpClient client = HttpClient.newHttpClient()) {

String mutation =
"""
mutation($pullRequestId: ID!, $mergeMethod: PullRequestMergeMethod!) {
enablePullRequestAutoMerge(input: {
pullRequestId: $pullRequestId,
mergeMethod: $mergeMethod,
}) {
clientMutationId
}
}
""";

ImmutableMap<String, Object> variables =
ImmutableMap.of(
"pullRequestId", pullRequest.getNodeId(), "mergeMethod", autoMergeType.name());

ImmutableMap<String, Object> payload =
ImmutableMap.of("query", mutation, "variables", variables);

String jsonPayload = objectMapper.writeValueAsStringUnchecked(payload);

String authToken =
getGithubAppInstallationToken(pullRequest.getRepository().getName()).getToken();

HttpRequest mutationRequest =
HttpRequest.newBuilder()
.uri(URI.create("https://api.github.com/graphql"))
.header("Authorization", "Bearer " + authToken)
.header("Content-Type", "application/json")
.POST(HttpRequest.BodyPublishers.ofString(jsonPayload))
.build();
try {
mutationResponse = client.send(mutationRequest, HttpResponse.BodyHandlers.ofString());
} catch (IOException | InterruptedException e) {
throw new RuntimeException(e);
}

if (mutationResponse.statusCode() != 200) {
throw new GithubException("Error enabling auto-merge: " + mutationResponse.body());
}

Map<String, Object> mutationResponseMap =
objectMapper.readValueUnchecked(mutationResponse.body(), new TypeReference<>() {});

if (mutationResponseMap.containsKey("errors")) {
throw new GithubException("Error enabling auto-merge: " + mutationResponse.body());
}
}
}

public String getOwner() {
return owner;
}
Expand All @@ -357,22 +445,25 @@ public String getEndpoint() {
return endpoint;
}

public GHAppInstallationToken getGithubAppInstallationToken(String repository)
throws IOException, NoSuchAlgorithmException, InvalidKeySpecException {
if (githubAppInstallationToken == null
|| githubAppInstallationToken.getExpiresAt().getTime()
<= System.currentTimeMillis() - 30000) {
// Existing installation token has less than 30 seconds before expiry, get new token
GitHub gitHub =
new GitHubBuilder()
.withEndpoint(getEndpoint())
.withJwtToken(getGithubJWT(tokenTTL).getToken())
.build();
githubAppInstallationToken =
gitHub.getApp().getInstallationByRepository(owner, repository).createToken().create();
}
public GHAppInstallationToken getGithubAppInstallationToken(String repository) {
try {
if (githubAppInstallationToken == null
|| githubAppInstallationToken.getExpiresAt().getTime()
<= System.currentTimeMillis() - 30000) {
// Existing installation token has less than 30 seconds before expiry, get new token
GitHub gitHub =
new GitHubBuilder()
.withEndpoint(getEndpoint())
.withJwtToken(getGithubJWT(tokenTTL).getToken())
.build();
githubAppInstallationToken =
gitHub.getApp().getInstallationByRepository(owner, repository).createToken().create();
}

return githubAppInstallationToken;
return githubAppInstallationToken;
} catch (Exception e) {
throw new RuntimeException("Can't get the App installation token", e);
}
}

protected GitHub createGithubClient(String repository)
Expand All @@ -383,7 +474,7 @@ protected GitHub createGithubClient(String repository)
.build();
}

private String getRepositoryPath(String repository) {
String getRepositoryPath(String repository) {
return owner != null && !owner.isEmpty() ? owner + "/" + repository : repository;
}

Expand Down

0 comments on commit be02edf

Please sign in to comment.