Skip to content

FEAT: Implement enabling auto merge for PR by GraphQL #2056

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 10 commits into from
Mar 19, 2025
Merged
60 changes: 60 additions & 0 deletions src/main/java/org/kohsuke/github/GHPullRequest.java
Original file line number Diff line number Diff line change
Expand Up @@ -629,6 +629,66 @@ public enum MergeMethod {
REBASE
}

/**
* Request to enable auto merge for a pull request.
*
* @param authorEmail
* The email address to associate with this merge.
* @param clientMutationId
* A unique identifier for the client performing the mutation.
* @param commitBody
* Commit body to use for the commit when the PR is mergable; if omitted, a default message will be used.
* NOTE: when merging with a merge queue any input value for commit message is ignored.
* @param commitHeadline
* Commit headline to use for the commit when the PR is mergable; if omitted, a default message will be
* used. NOTE: when merging with a merge queue any input value for commit headline is ignored.
* @param expectedHeadOid
* The expected head OID of the pull request.
* @param mergeMethod
* The merge method to use. If omitted, defaults to `MERGE`. NOTE: when merging with a merge queue any
* input value for merge method is ignored.
* @throws IOException
* the io exception
*/
public void enablePullRequestAutoMerge(String authorEmail,
String clientMutationId,
String commitBody,
String commitHeadline,
String expectedHeadOid,
MergeMethod mergeMethod) throws IOException {

StringBuilder inputBuilder = new StringBuilder();
addParameter(inputBuilder, "pullRequestId", this.getNodeId());
addOptionalParameter(inputBuilder, "authorEmail", authorEmail);
addOptionalParameter(inputBuilder, "clientMutationId", clientMutationId);
addOptionalParameter(inputBuilder, "commitBody", commitBody);
addOptionalParameter(inputBuilder, "commitHeadline", commitHeadline);
addOptionalParameter(inputBuilder, "expectedHeadOid", expectedHeadOid);
addOptionalParameter(inputBuilder, "mergeMethod", mergeMethod);

String graphqlBody = "mutation EnableAutoMerge { enablePullRequestAutoMerge(input: {" + inputBuilder + "}) { "
+ "pullRequest { id } } }";

root().createGraphQLRequest(graphqlBody).sendGraphQL();

refresh();
}

private void addOptionalParameter(StringBuilder inputBuilder, String name, Object value) {
if (value != null) {
addParameter(inputBuilder, name, value);
}
}

private void addParameter(StringBuilder inputBuilder, String name, Object value) {
Objects.requireNonNull(value);
String formatString = " %s: \"%s\"";
if (value instanceof Enum) {
formatString = " %s: %s";
}

inputBuilder.append(String.format(formatString, name, value));
}
/**
* The status of auto merging a {@linkplain GHPullRequest}.
*
Expand Down
16 changes: 16 additions & 0 deletions src/main/java/org/kohsuke/github/GitHub.java
Original file line number Diff line number Diff line change
Expand Up @@ -1299,6 +1299,22 @@ Requester createRequest() {
return requester;
}

/**
* Creates a request to GitHub GraphQL API.
*
* @param query
* the query for the GraphQL
* @return the requester
*/
@Nonnull
Requester createGraphQLRequest(String query) {
return createRequest()
.method("POST")
.rateLimit(RateLimitTarget.GRAPHQL)
.with("query", query)
.withUrlPath("/graphql");
}

/**
* Intern.
*
Expand Down
32 changes: 32 additions & 0 deletions src/main/java/org/kohsuke/github/Requester.java
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
import org.apache.commons.io.IOUtils;
import org.kohsuke.github.connector.GitHubConnectorResponse;
import org.kohsuke.github.function.InputStreamFunction;
import org.kohsuke.github.internal.graphql.response.GHGraphQLResponse;

import java.io.ByteArrayInputStream;
import java.io.IOException;
Expand Down Expand Up @@ -103,6 +104,37 @@ public <T> T fetchInto(@Nonnull T existingInstance) throws IOException {
.body();
}

/**
* Sends a GraphQL request with no response
*
* @throws IOException
* the io exception
*/
public void sendGraphQL() throws IOException {
fetchGraphQL(GHGraphQLResponse.ObjectResponse.class);
}

/**
* Sends a request and parses the response into the given type via databinding in GraphQL response.
*
* @param <T>
* the type parameter
* @param type
* the type
* @return an instance of {@code GHGraphQLResponse<T>}
* @throws IOException
* if the server returns 4xx/5xx responses.
*/
public <T extends GHGraphQLResponse<S>, S> S fetchGraphQL(@Nonnull Class<T> type) throws IOException {
T response = fetch(type);

if (!response.isSuccessful()) {
throw new IOException("GraphQL request failed by:" + response.getErrorMessages());
}

return response.getData();
}

/**
* Makes a request and just obtains the HTTP status code. Method does not throw exceptions for many status codes
* that would otherwise throw.
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
package org.kohsuke.github.internal.graphql.response;

import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonProperty;
import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;

import java.util.Collections;
import java.util.List;
import java.util.stream.Collectors;

/**
* A response of GraphQL.
* <p>
* This class is used to parse the response of GraphQL.
* </p>
*
* @param <T>
* the type of data
*/
public class GHGraphQLResponse<T> {

private final T data;

private final List<GraphQLError> errors;

/**
* @param data
* GraphQL success response
* @param errors
* GraphQL failure response, This will be empty if not fail
*/
@JsonCreator
@SuppressFBWarnings(value = { "EI_EXPOSE_REP2" }, justification = "Spotbugs also doesn't like this")
public GHGraphQLResponse(@JsonProperty("data") T data, @JsonProperty("errors") List<GraphQLError> errors) {
if (errors == null) {
errors = Collections.emptyList();
}
this.data = data;
this.errors = Collections.unmodifiableList(errors);
}

/**
* @return request is succeeded. True when error list is empty.
*/
public boolean isSuccessful() {
return errors.isEmpty();
}

/**
* @return GraphQL success response
*/
public T getData() {
if (!isSuccessful()) {
throw new RuntimeException("Response not successful, data invalid");
}

return data;
}

/**
* @return GraphQL error messages from Github Response. Empty list when no errors occurred.
*/
public List<String> getErrorMessages() {
return errors.stream().map(GraphQLError::getMessage).collect(Collectors.toList());
}

/**
* A error of GraphQL response. Minimum implementation for GraphQL error.
*/
@SuppressFBWarnings(value = { "UWF_UNWRITTEN_FIELD", "UWF_FIELD_NOT_INITIALIZED_IN_CONSTRUCTOR" },
justification = "JSON API")
private static class GraphQLError {
private String message;

public String getMessage() {
return message;
}
}

/**
* A GraphQL response with basic Object data type.
*/
public static class ObjectResponse extends GHGraphQLResponse<Object> {
/**
* {@inheritDoc}
*/
@JsonCreator
@SuppressFBWarnings(value = { "EI_EXPOSE_REP2" }, justification = "Spotbugs also doesn't like this")
public ObjectResponse(@JsonProperty("data") Object data, @JsonProperty("errors") List<GraphQLError> errors) {
super(data, errors);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -6703,5 +6703,95 @@
"allDeclaredMethods": true,
"allPublicClasses": true,
"allDeclaredClasses": true
},
{
"name": "org.kohsuke.github.GHPullRequest$EnablePullRequestAutoMergeResponse",
"allPublicFields": true,
"allDeclaredFields": true,
"queryAllPublicConstructors": true,
"queryAllDeclaredConstructors": true,
"allPublicConstructors": true,
"allDeclaredConstructors": true,
"queryAllPublicMethods": true,
"queryAllDeclaredMethods": true,
"allPublicMethods": true,
"allDeclaredMethods": true,
"allPublicClasses": true,
"allDeclaredClasses": true
},
{
"name": "org.kohsuke.github.GHPullRequest$EnablePullRequestAutoMergeResponse$EnablePullRequestAutoMerge",
"allPublicFields": true,
"allDeclaredFields": true,
"queryAllPublicConstructors": true,
"queryAllDeclaredConstructors": true,
"allPublicConstructors": true,
"allDeclaredConstructors": true,
"queryAllPublicMethods": true,
"queryAllDeclaredMethods": true,
"allPublicMethods": true,
"allDeclaredMethods": true,
"allPublicClasses": true,
"allDeclaredClasses": true
},
{
"name": "org.kohsuke.github.GHPullRequest$EnablePullRequestAutoMergeResponse$EnablePullRequestAutoMerge$EnablePullRequestAutoMergePullRequest",
"allPublicFields": true,
"allDeclaredFields": true,
"queryAllPublicConstructors": true,
"queryAllDeclaredConstructors": true,
"allPublicConstructors": true,
"allDeclaredConstructors": true,
"queryAllPublicMethods": true,
"queryAllDeclaredMethods": true,
"allPublicMethods": true,
"allDeclaredMethods": true,
"allPublicClasses": true,
"allDeclaredClasses": true
},
{
"name": "org.kohsuke.github.internal.graphql.response.GHGraphQLResponse",
"allPublicFields": true,
"allDeclaredFields": true,
"queryAllPublicConstructors": true,
"queryAllDeclaredConstructors": true,
"allPublicConstructors": true,
"allDeclaredConstructors": true,
"queryAllPublicMethods": true,
"queryAllDeclaredMethods": true,
"allPublicMethods": true,
"allDeclaredMethods": true,
"allPublicClasses": true,
"allDeclaredClasses": true
},
{
"name": "org.kohsuke.github.internal.graphql.response.GHGraphQLResponse$GraphQLError",
"allPublicFields": true,
"allDeclaredFields": true,
"queryAllPublicConstructors": true,
"queryAllDeclaredConstructors": true,
"allPublicConstructors": true,
"allDeclaredConstructors": true,
"queryAllPublicMethods": true,
"queryAllDeclaredMethods": true,
"allPublicMethods": true,
"allDeclaredMethods": true,
"allPublicClasses": true,
"allDeclaredClasses": true
},
{
"name": "org.kohsuke.github.internal.graphql.response.GHGraphQLResponse$ObjectResponse",
"allPublicFields": true,
"allDeclaredFields": true,
"queryAllPublicConstructors": true,
"queryAllDeclaredConstructors": true,
"allPublicConstructors": true,
"allDeclaredConstructors": true,
"queryAllPublicMethods": true,
"queryAllDeclaredMethods": true,
"allPublicMethods": true,
"allDeclaredMethods": true,
"allPublicClasses": true,
"allDeclaredClasses": true
}
]
Original file line number Diff line number Diff line change
Expand Up @@ -1342,5 +1342,23 @@
},
{
"name": "org.kohsuke.github.SkipFromToString"
},
{
"name": "org.kohsuke.github.GHPullRequest$EnablePullRequestAutoMergeResponse"
},
{
"name": "org.kohsuke.github.GHPullRequest$EnablePullRequestAutoMergeResponse$EnablePullRequestAutoMerge"
},
{
"name": "org.kohsuke.github.GHPullRequest$EnablePullRequestAutoMergeResponse$EnablePullRequestAutoMerge$EnablePullRequestAutoMergePullRequest"
},
{
"name": "org.kohsuke.github.internal.graphql.response.GHGraphQLResponse"
},
{
"name": "org.kohsuke.github.internal.graphql.response.GHGraphQLResponse$GraphQLError"
},
{
"name": "org.kohsuke.github.internal.graphql.response.GHGraphQLResponse$ObjectResponse"
}
]
Original file line number Diff line number Diff line change
Expand Up @@ -33,5 +33,4 @@ public void shouldMockGHPullRequest() throws IOException {

assertThat("Mock should return true", pullRequest.isDraft());
}

}
Loading
Loading