diff --git a/pom.xml b/pom.xml index 9db3d20ba46e2cf98903080bce48c4ef43aec257..98925c046dffd638fe1d099687cfff7a74652177 100644 --- a/pom.xml +++ b/pom.xml @@ -5,7 +5,7 @@ <artifactId>git-eca</artifactId> <version>1.1.0</version> <properties> - <eclipse-api-version>0.7.1</eclipse-api-version> + <eclipse-api-version>0.7.3</eclipse-api-version> <compiler-plugin.version>3.8.1</compiler-plugin.version> <maven.compiler.parameters>true</maven.compiler.parameters> <maven.compiler.source>11</maven.compiler.source> diff --git a/src/main/java/org/eclipsefoundation/git/eca/api/GithubAPI.java b/src/main/java/org/eclipsefoundation/git/eca/api/GithubAPI.java index 4f4032b46fc5e62238d1e63829ca3855df1df378..292d6f3c88c0bc7ef7a1852d12e8c02705bc4ffb 100644 --- a/src/main/java/org/eclipsefoundation/git/eca/api/GithubAPI.java +++ b/src/main/java/org/eclipsefoundation/git/eca/api/GithubAPI.java @@ -11,6 +11,7 @@ */ package org.eclipsefoundation.git.eca.api; +import javax.ws.rs.BeanParam; import javax.ws.rs.GET; import javax.ws.rs.HeaderParam; import javax.ws.rs.POST; @@ -20,8 +21,10 @@ import javax.ws.rs.Produces; import javax.ws.rs.core.Response; import org.eclipse.microprofile.rest.client.inject.RegisterRestClient; +import org.eclipsefoundation.core.service.APIMiddleware.BaseAPIParameters; import org.eclipsefoundation.git.eca.api.models.GithubAccessToken; import org.eclipsefoundation.git.eca.api.models.GithubCommitStatusRequest; +import org.eclipsefoundation.git.eca.api.models.GithubWebhookRequest.PullRequest; import org.jboss.resteasy.util.HttpHeaderNames; /** @@ -34,19 +37,88 @@ import org.jboss.resteasy.util.HttpHeaderNames; @Produces("application/json") public interface GithubAPI { + /** + * Retrieves information about a certain pull request in a repo if it exists + * + * @param bearer authorization header value, access token for application with access to repo + * @param apiVersion the version of the GH API to target when making the request + * @param repoFull the full repo name that is being targeted + * @param pullNumber the pull request number + * @return information about the given pull request if it exists. + */ + @GET + @Path("repos/{repoFull}/pulls/{pullNumber}") + public PullRequest getPullRequest(@HeaderParam(HttpHeaderNames.AUTHORIZATION) String bearer, + @HeaderParam("X-GitHub-Api-Version") String apiVersion, @PathParam("repoFull") String repoFull, + @PathParam("pullNumber") int pullNumber); + + /** + * Retrieves a list of commits related to the given pull request. + * + * @param bearer authorization header value, access token for application with access to repo + * @param apiVersion the version of the GH API to target when making the request + * @param repoFull the full repo name that is being targeted + * @param pullNumber the pull request number + * @return list of commits associated with the pull request, wrapped in a jax-rs response + */ @GET @Path("repos/{repoFull}/pulls/{pullNumber}/commits") public Response getCommits(@HeaderParam(HttpHeaderNames.AUTHORIZATION) String bearer, @HeaderParam("X-GitHub-Api-Version") String apiVersion, @PathParam("repoFull") String repoFull, @PathParam("pullNumber") int pullNumber); + /** + * Posts an update to the Github API, using an access token to update a given pull requests commit status, targeted + * using the head sha. + * + * @param bearer authorization header value, access token for application with access to repo + * @param apiVersion the version of the GH API to target when making the request + * @param repoFull the full repo name that is being targeted + * @param prHeadSha the head SHA for the request + * @param commitStatusUpdate the status body sent with the request + * @return JAX-RS response to check for success or failure based on status code. + */ @POST @Path("repos/{repoFull}/statuses/{prHeadSha}") - public Response updateStatus(@HeaderParam(HttpHeaderNames.AUTHORIZATION) String bearer, @HeaderParam("X-GitHub-Api-Version") String apiVersion, - @PathParam("repoFull") String repoFull, @PathParam("prHeadSha") String prHeadSha, GithubCommitStatusRequest commitStatusUpdate); + public Response updateStatus(@HeaderParam(HttpHeaderNames.AUTHORIZATION) String bearer, + @HeaderParam("X-GitHub-Api-Version") String apiVersion, @PathParam("repoFull") String repoFull, + @PathParam("prHeadSha") String prHeadSha, GithubCommitStatusRequest commitStatusUpdate); + /** + * Requires a JWT bearer token for the application to retrieve installations for. Returns a list of installations for + * the given application. + * + * @param params the general params for requests, including pagination + * @param bearer JWT bearer token for the target application + * @return list of installations for the application + */ + @GET + @Path("app/installations") + public Response getInstallations(@BeanParam BaseAPIParameters params, @HeaderParam(HttpHeaderNames.AUTHORIZATION) String bearer); + + /** + * Retrieves an access token for a specific installation, given the applications JWT bearer and the api version. + * + * @param bearer the authorization header value, should contain the apps signed JWT token + * @param apiVersion the API version to target with the request + * @param installationId the installation to generate an access token for + * @return the Github access token for the GH app installation + */ @POST @Path("app/installations/{installationId}/access_tokens") public GithubAccessToken getNewAccessToken(@HeaderParam(HttpHeaderNames.AUTHORIZATION) String bearer, @HeaderParam("X-GitHub-Api-Version") String apiVersion, @PathParam("installationId") String installationId); + + /** + * Returns a list of repositories for the given installation. + * + * @param params the general params for requests, including pagination + * @param bearer JWT bearer token for the target installation + * @return list of repositories for the installation as a response for pagination + */ + @GET + @Path("installation/repositories") + public Response getInstallationRepositories(@BeanParam BaseAPIParameters params, + @HeaderParam(HttpHeaderNames.AUTHORIZATION) String bearer); + } diff --git a/src/main/java/org/eclipsefoundation/git/eca/api/models/GithubAccessToken.java b/src/main/java/org/eclipsefoundation/git/eca/api/models/GithubAccessToken.java index 437e4a6c19466e7864c22203a7a6db409c4602e1..887e1cb7dae71064e0bdd46e2e5f89539ccbe2f7 100644 --- a/src/main/java/org/eclipsefoundation/git/eca/api/models/GithubAccessToken.java +++ b/src/main/java/org/eclipsefoundation/git/eca/api/models/GithubAccessToken.java @@ -18,7 +18,9 @@ import com.fasterxml.jackson.databind.annotation.JsonPOJOBuilder; import com.google.auto.value.AutoValue; /** - * @author martin + * Contains the information about the Github application access tokens + * + * @author Martin Lowe * */ @AutoValue @@ -26,8 +28,9 @@ import com.google.auto.value.AutoValue; public abstract class GithubAccessToken { public abstract String getToken(); + public abstract LocalDateTime getExpiresAt(); - + public static Builder builder() { return new AutoValue_GithubAccessToken.Builder(); } @@ -36,6 +39,7 @@ public abstract class GithubAccessToken { @JsonPOJOBuilder(withPrefix = "set") public abstract static class Builder { public abstract Builder setToken(String token); + public abstract Builder setExpiresAt(LocalDateTime expiresAt); public abstract GithubAccessToken build(); diff --git a/src/main/java/org/eclipsefoundation/git/eca/api/models/GithubApplicationInstallation.java b/src/main/java/org/eclipsefoundation/git/eca/api/models/GithubApplicationInstallation.java new file mode 100644 index 0000000000000000000000000000000000000000..3783a36b912a0e5e3bc15e9197447ad3562b3826 --- /dev/null +++ b/src/main/java/org/eclipsefoundation/git/eca/api/models/GithubApplicationInstallation.java @@ -0,0 +1,49 @@ +/** + * Copyright (c) 2023 Eclipse Foundation + * + * This program and the accompanying materials are made + * available under the terms of the Eclipse Public License 2.0 + * which is available at https://www.eclipse.org/legal/epl-2.0/ + * + * Author: Martin Lowe <martin.lowe@eclipse-foundation.org> + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.eclipsefoundation.git.eca.api.models; + +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import com.fasterxml.jackson.databind.annotation.JsonPOJOBuilder; +import com.google.auto.value.AutoValue; + +/** + * Information about the given Github ECA application installation + * + * @author Martin Lowe + * + */ +@AutoValue +@JsonDeserialize(builder = AutoValue_GithubApplicationInstallation.Builder.class) +public abstract class GithubApplicationInstallation { + + public abstract int getId(); + + public abstract String getTargetType(); + + public abstract String getTargetId(); + + public static Builder builder() { + return new AutoValue_GithubApplicationInstallation.Builder(); + } + + @AutoValue.Builder + @JsonPOJOBuilder(withPrefix = "set") + public abstract static class Builder { + public abstract Builder setId(int id); + + public abstract Builder setTargetType(String targetType); + + public abstract Builder setTargetId(String targetId); + + public abstract GithubApplicationInstallation build(); + } +} diff --git a/src/main/java/org/eclipsefoundation/git/eca/api/models/GithubInstallationRepositoriesResponse.java b/src/main/java/org/eclipsefoundation/git/eca/api/models/GithubInstallationRepositoriesResponse.java new file mode 100644 index 0000000000000000000000000000000000000000..7c81446e41432330070e8fd51053d3ab9f025849 --- /dev/null +++ b/src/main/java/org/eclipsefoundation/git/eca/api/models/GithubInstallationRepositoriesResponse.java @@ -0,0 +1,45 @@ +/** + * Copyright (c) 2023 Eclipse Foundation + * + * This program and the accompanying materials are made + * available under the terms of the Eclipse Public License 2.0 + * which is available at https://www.eclipse.org/legal/epl-2.0/ + * + * Author: Martin Lowe <martin.lowe@eclipse-foundation.org> + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.eclipsefoundation.git.eca.api.models; + +import java.util.List; + +import org.eclipsefoundation.git.eca.api.models.GithubWebhookRequest.Repository; + +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import com.fasterxml.jackson.databind.annotation.JsonPOJOBuilder; +import com.google.auto.value.AutoValue; + +/** + * Model response for /installations/repositories + * + * @author Martin Lowe + */ +@AutoValue +@JsonDeserialize(builder = AutoValue_GithubInstallationRepositoriesResponse.Builder.class) +public abstract class GithubInstallationRepositoriesResponse { + + public abstract List<Repository> getRepositories(); + + public static Builder builder() { + return new AutoValue_GithubInstallationRepositoriesResponse.Builder(); + } + + @AutoValue.Builder + @JsonPOJOBuilder(withPrefix = "set") + public abstract static class Builder { + + public abstract Builder setRepositories(List<Repository> repository); + + public abstract GithubInstallationRepositoriesResponse build(); + } +} diff --git a/src/main/java/org/eclipsefoundation/git/eca/api/models/GithubWebhookRequest.java b/src/main/java/org/eclipsefoundation/git/eca/api/models/GithubWebhookRequest.java index 90d8b50ba8237e94fb027933e3b49951bf0b8f1e..5bf0d7dbc72c80ed7eda7d74b7918c7227831f0b 100644 --- a/src/main/java/org/eclipsefoundation/git/eca/api/models/GithubWebhookRequest.java +++ b/src/main/java/org/eclipsefoundation/git/eca/api/models/GithubWebhookRequest.java @@ -104,6 +104,8 @@ public abstract class GithubWebhookRequest { @JsonDeserialize(builder = AutoValue_GithubWebhookRequest_PullRequest.Builder.class) public abstract static class PullRequest { + public abstract String getState(); + public abstract Integer getNumber(); public abstract PullRequestHead getHead(); @@ -115,6 +117,8 @@ public abstract class GithubWebhookRequest { @AutoValue.Builder @JsonPOJOBuilder(withPrefix = "set") public abstract static class Builder { + public abstract Builder setState(String state); + public abstract Builder setNumber(Integer number); public abstract Builder setHead(PullRequestHead head); diff --git a/src/main/java/org/eclipsefoundation/git/eca/dto/CommitValidationStatus.java b/src/main/java/org/eclipsefoundation/git/eca/dto/CommitValidationStatus.java index 14a21fc8b77bb7d885abe862d66529405b3f5ab5..7c82d88fff239c59c4854064017d138545769912 100644 --- a/src/main/java/org/eclipsefoundation/git/eca/dto/CommitValidationStatus.java +++ b/src/main/java/org/eclipsefoundation/git/eca/dto/CommitValidationStatus.java @@ -46,6 +46,7 @@ public class CommitValidationStatus extends BareNode { @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; private String commitHash; + private String userMail; private String project; private String repoUrl; @Enumerated(EnumType.STRING) @@ -79,6 +80,20 @@ public class CommitValidationStatus extends BareNode { this.commitHash = commitHash; } + /** + * @return the userMail + */ + public String getUserMail() { + return userMail; + } + + /** + * @param userMail the userMail to set + */ + public void setUserMail(String userMail) { + this.userMail = userMail; + } + /** * @return the project */ @@ -184,6 +199,8 @@ public class CommitValidationStatus extends BareNode { builder.append(id); builder.append(", sha="); builder.append(commitHash); + builder.append(", userMail="); + builder.append(userMail); builder.append(", project="); builder.append(project); builder.append(", repoUrl="); @@ -213,26 +230,25 @@ public class CommitValidationStatus extends BareNode { // sha check String commitHash = params.getFirst(GitEcaParameterNames.SHA.getName()); if (StringUtils.isNumeric(commitHash)) { - stmt.addClause( - new ParameterizedSQLStatement.Clause(TABLE.getAlias() + ".commitHash = ?", - new Object[] { commitHash })); + stmt.addClause(new ParameterizedSQLStatement.Clause(TABLE.getAlias() + ".commitHash = ?", new Object[] { commitHash })); } String projectId = params.getFirst(GitEcaParameterNames.PROJECT_ID.getName()); if (StringUtils.isNumeric(projectId)) { - stmt.addClause(new ParameterizedSQLStatement.Clause(TABLE.getAlias() + ".projectId = ?", - new Object[] { projectId })); + stmt.addClause(new ParameterizedSQLStatement.Clause(TABLE.getAlias() + ".projectId = ?", new Object[] { projectId })); } List<String> commitHashes = params.get(GitEcaParameterNames.SHAS.getName()); if (commitHashes != null && !commitHashes.isEmpty()) { - stmt.addClause( - new ParameterizedSQLStatement.Clause(TABLE.getAlias() + ".commitHash IN ?", - new Object[] { commitHashes })); + stmt.addClause(new ParameterizedSQLStatement.Clause(TABLE.getAlias() + ".commitHash IN ?", new Object[] { commitHashes })); } String repoUrl = params.getFirst(GitEcaParameterNames.REPO_URL.getName()); if (StringUtils.isNotBlank(repoUrl)) { - stmt.addClause(new ParameterizedSQLStatement.Clause(TABLE.getAlias() + ".repoUrl = ?", - new Object[] { repoUrl })); + stmt.addClause(new ParameterizedSQLStatement.Clause(TABLE.getAlias() + ".repoUrl = ?", new Object[] { repoUrl })); } + String userMail = params.getFirst(GitEcaParameterNames.USER_MAIL.getName()); + if (StringUtils.isNotBlank(userMail)) { + stmt.addClause(new ParameterizedSQLStatement.Clause(TABLE.getAlias() + ".userMail = ?", new Object[] { userMail })); + } + return stmt; } diff --git a/src/main/java/org/eclipsefoundation/git/eca/dto/GithubWebhookTracking.java b/src/main/java/org/eclipsefoundation/git/eca/dto/GithubWebhookTracking.java index 4135793778db40a2705c4c07d4b133492cb6232e..e1c5ebe1280cd2039e473407e4d44db3a778dfa8 100644 --- a/src/main/java/org/eclipsefoundation/git/eca/dto/GithubWebhookTracking.java +++ b/src/main/java/org/eclipsefoundation/git/eca/dto/GithubWebhookTracking.java @@ -209,6 +209,24 @@ public class GithubWebhookTracking extends BareNode { .addClause( new ParameterizedSQLStatement.Clause(TABLE.getAlias() + ".fingerprint = ?", new Object[] { fingerprint })); } + String installationId = params.getFirst(GitEcaParameterNames.INSTALLATION_ID_RAW); + if (StringUtils.isNotBlank(installationId)) { + statement + .addClause(new ParameterizedSQLStatement.Clause(TABLE.getAlias() + ".installationId = ?", + new Object[] { installationId })); + } + String pullRequestNumber = params.getFirst(GitEcaParameterNames.PULL_REQUEST_NUMBER_RAW); + if (StringUtils.isNumeric(pullRequestNumber)) { + statement + .addClause(new ParameterizedSQLStatement.Clause(TABLE.getAlias() + ".pullRequestNumber = ?", + new Object[] { Integer.parseInt(pullRequestNumber) })); + } + String repositoryFullName = params.getFirst(GitEcaParameterNames.REPOSITORY_FULL_NAME_RAW); + if (StringUtils.isNotBlank(repositoryFullName)) { + statement + .addClause(new ParameterizedSQLStatement.Clause(TABLE.getAlias() + ".repositoryFullName = ?", + new Object[] { repositoryFullName })); + } return statement; } diff --git a/src/main/java/org/eclipsefoundation/git/eca/helper/JwtHelper.java b/src/main/java/org/eclipsefoundation/git/eca/helper/JwtHelper.java index 33d22c81224149bf2f036c092969b73d7798b2c1..3fd168db02f562e8972c389950d032ce419ea720 100644 --- a/src/main/java/org/eclipsefoundation/git/eca/helper/JwtHelper.java +++ b/src/main/java/org/eclipsefoundation/git/eca/helper/JwtHelper.java @@ -80,7 +80,7 @@ public class JwtHelper { * * @return signed JWT using the issuer and secret defined in the secret properties. */ - private String generateJwt() { + public String generateJwt() { return Jwt.subject("EclipseWebmaster").sign(JwtHelper.getExternalPrivateKey(location)); } diff --git a/src/main/java/org/eclipsefoundation/git/eca/model/Commit.java b/src/main/java/org/eclipsefoundation/git/eca/model/Commit.java index e13ed00850fad948eef180475c56120acc388d40..a140d7d06cf478fa012f225fff2be2504c36356c 100644 --- a/src/main/java/org/eclipsefoundation/git/eca/model/Commit.java +++ b/src/main/java/org/eclipsefoundation/git/eca/model/Commit.java @@ -11,6 +11,7 @@ **********************************************************************/ package org.eclipsefoundation.git.eca.model; +import java.time.ZonedDateTime; import java.util.ArrayList; import java.util.List; @@ -29,48 +30,50 @@ import com.google.auto.value.AutoValue; @AutoValue @JsonDeserialize(builder = AutoValue_Commit.Builder.class) public abstract class Commit { - @Nullable - public abstract String getHash(); + public abstract String getHash(); - @Nullable - public abstract String getSubject(); + @Nullable + public abstract String getSubject(); - @Nullable - public abstract String getBody(); + @Nullable + public abstract String getBody(); - @Nullable - public abstract List<String> getParents(); + @Nullable + public abstract List<String> getParents(); - @Nullable - public abstract GitUser getAuthor(); + public abstract GitUser getAuthor(); - @Nullable - public abstract GitUser getCommitter(); + public abstract GitUser getCommitter(); - @Nullable - public abstract Boolean getHead(); + @Nullable + public abstract Boolean getHead(); - public static Builder builder() { - return new AutoValue_Commit.Builder().setParents(new ArrayList<>()); - } + @Nullable + public abstract ZonedDateTime getLastModificationDate(); - @AutoValue.Builder - @JsonPOJOBuilder(withPrefix = "set") - public abstract static class Builder { - public abstract Builder setHash(@Nullable String hash); + public static Builder builder() { + return new AutoValue_Commit.Builder().setParents(new ArrayList<>()); + } - public abstract Builder setSubject(@Nullable String subject); + @AutoValue.Builder + @JsonPOJOBuilder(withPrefix = "set") + public abstract static class Builder { + public abstract Builder setHash(String hash); - public abstract Builder setBody(@Nullable String body); + public abstract Builder setSubject(@Nullable String subject); - public abstract Builder setParents(@Nullable List<String> parents); + public abstract Builder setBody(@Nullable String body); - public abstract Builder setAuthor(@Nullable GitUser author); + public abstract Builder setParents(@Nullable List<String> parents); - public abstract Builder setCommitter(@Nullable GitUser committer); + public abstract Builder setAuthor(GitUser author); - public abstract Builder setHead(@Nullable Boolean head); + public abstract Builder setCommitter(GitUser committer); - public abstract Commit build(); - } + public abstract Builder setHead(@Nullable Boolean head); + + public abstract Builder setLastModificationDate(@Nullable ZonedDateTime lastModificationDate); + + public abstract Commit build(); + } } diff --git a/src/main/java/org/eclipsefoundation/git/eca/namespace/GitEcaParameterNames.java b/src/main/java/org/eclipsefoundation/git/eca/namespace/GitEcaParameterNames.java index e75ca1706576d3a16a2a60ce6fa950c393d65b8d..a16499237e51d4313a1e7505d0e73f187f702327 100644 --- a/src/main/java/org/eclipsefoundation/git/eca/namespace/GitEcaParameterNames.java +++ b/src/main/java/org/eclipsefoundation/git/eca/namespace/GitEcaParameterNames.java @@ -35,6 +35,10 @@ public final class GitEcaParameterNames implements UrlParameterNamespace { public static final String STATUS_DELETED_RAW = "deleted"; public static final String SINCE_RAW = "since"; public static final String UNTIL_RAW = "until"; + public static final String REPOSITORY_FULL_NAME_RAW = "repository_full_name"; + public static final String INSTALLATION_ID_RAW = "installation_id"; + public static final String PULL_REQUEST_NUMBER_RAW = "pull_request_number"; + public static final String USER_MAIL_RAW = "user_mail"; public static final UrlParameter COMMIT_ID = new UrlParameter(COMMIT_ID_RAW); public static final UrlParameter SHA = new UrlParameter(SHA_RAW); public static final UrlParameter SHAS = new UrlParameter(SHAS_RAW); @@ -50,11 +54,17 @@ public final class GitEcaParameterNames implements UrlParameterNamespace { public static final UrlParameter STATUS_DELETED = new UrlParameter(STATUS_DELETED_RAW); public static final UrlParameter SINCE = new UrlParameter(SINCE_RAW); public static final UrlParameter UNTIL = new UrlParameter(UNTIL_RAW); + public static final UrlParameter REPOSITORY_FULL_NAME = new UrlParameter(REPOSITORY_FULL_NAME_RAW); + public static final UrlParameter INSTALLATION_ID = new UrlParameter(INSTALLATION_ID_RAW); + public static final UrlParameter PULL_REQUEST_NUMBER = new UrlParameter(PULL_REQUEST_NUMBER_RAW); + public static final UrlParameter USER_MAIL = new UrlParameter(USER_MAIL_RAW); @Override public List<UrlParameter> getParameters() { - return Arrays.asList(COMMIT_ID, SHA, SHAS, PROJECT_ID, PROJECT_IDS, NOT_IN_PROJECT_IDS, REPO_URL, - FINGERPRINT, USER_ID, PROJECT_PATH, PARENT_PROJECT, STATUS_ACTIVE, STATUS_DELETED, SINCE, UNTIL); + return Arrays + .asList(COMMIT_ID, SHA, SHAS, PROJECT_ID, PROJECT_IDS, NOT_IN_PROJECT_IDS, REPO_URL, FINGERPRINT, USER_ID, PROJECT_PATH, + PARENT_PROJECT, STATUS_ACTIVE, STATUS_DELETED, SINCE, UNTIL, REPOSITORY_FULL_NAME, INSTALLATION_ID, + PULL_REQUEST_NUMBER, USER_MAIL); } } diff --git a/src/main/java/org/eclipsefoundation/git/eca/resource/GithubAdjacentResource.java b/src/main/java/org/eclipsefoundation/git/eca/resource/GithubAdjacentResource.java new file mode 100644 index 0000000000000000000000000000000000000000..8393c154aaf1154fb0e2b868bc17bf701ad916b0 --- /dev/null +++ b/src/main/java/org/eclipsefoundation/git/eca/resource/GithubAdjacentResource.java @@ -0,0 +1,233 @@ +/** + * Copyright (c) 2023 Eclipse Foundation + * + * This program and the accompanying materials are made + * available under the terms of the Eclipse Public License 2.0 + * which is available at https://www.eclipse.org/legal/epl-2.0/ + * + * Author: Martin Lowe <martin.lowe@eclipse-foundation.org> + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.eclipsefoundation.git.eca.resource; + +import java.net.URI; +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; +import java.util.stream.Collectors; + +import javax.inject.Inject; +import javax.ws.rs.BadRequestException; +import javax.ws.rs.core.MultivaluedMap; + +import org.apache.commons.lang3.StringUtils; +import org.eclipse.microprofile.config.inject.ConfigProperty; +import org.eclipse.microprofile.rest.client.inject.RestClient; +import org.eclipsefoundation.core.model.RequestWrapper; +import org.eclipsefoundation.core.service.APIMiddleware; +import org.eclipsefoundation.git.eca.api.GithubAPI; +import org.eclipsefoundation.git.eca.api.models.GithubCommit; +import org.eclipsefoundation.git.eca.api.models.GithubCommitStatusRequest; +import org.eclipsefoundation.git.eca.api.models.GithubWebhookRequest; +import org.eclipsefoundation.git.eca.dto.GithubWebhookTracking; +import org.eclipsefoundation.git.eca.helper.JwtHelper; +import org.eclipsefoundation.git.eca.model.Commit; +import org.eclipsefoundation.git.eca.model.GitUser; +import org.eclipsefoundation.git.eca.model.ValidationRequest; +import org.eclipsefoundation.git.eca.model.ValidationResponse; +import org.eclipsefoundation.git.eca.namespace.GitEcaParameterNames; +import org.eclipsefoundation.git.eca.namespace.GithubCommitStatuses; +import org.eclipsefoundation.git.eca.namespace.ProviderType; +import org.eclipsefoundation.git.eca.service.ValidationService; +import org.eclipsefoundation.persistence.dao.PersistenceDao; +import org.eclipsefoundation.persistence.model.RDBMSQuery; +import org.eclipsefoundation.persistence.service.FilterService; +import org.jboss.resteasy.specimpl.MultivaluedMapImpl; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Contains operations and properties that are common to resources that interact with Github validation. + * + * @author Martin Lowe + * + */ +public abstract class GithubAdjacentResource { + private static final Logger LOGGER = LoggerFactory.getLogger(GithubAdjacentResource.class); + + private static final String VALIDATION_LOGGING_MESSAGE = "Setting validation state for {}/#{} to {}"; + + @ConfigProperty(name = "eclipse.webhooks.github.context") + String context; + @ConfigProperty(name = "eclipse.webhooks.github.server-target") + String serverTarget; + @ConfigProperty(name = "eclipse.github.default-api-version", defaultValue = "2022-11-28") + String apiVersion; + + @Inject + JwtHelper jwtHelper; + @Inject + APIMiddleware middleware; + @Inject + PersistenceDao dao; + @Inject + FilterService filters; + + @Inject + ValidationService validationService; + + @Inject + RequestWrapper wrapper; + + @RestClient + GithubAPI ghApi; + + /** + * Generate a ValidationRequest object based on data pulled from Github, grabbing commits from the noted pull request + * using the installation ID for access/authorization. + * + * @param installationId the ECA app installation ID for the organization + * @param repositoryFullName the full name of the repository where the PR resides + * @param pullRequestNumber the pull request number that is being validated + * @param repositoryUrl the URL of the repository that contains the commits to validate + * @return the populated validation request for the Github request information + */ + ValidationRequest generateRequest(String installationId, String repositoryFullName, int pullRequestNumber, String repositoryUrl) { + checkRequestParameters(installationId, repositoryFullName, pullRequestNumber); + // get the commits that will be validated, don't cache as changes can come in too fast for it to be useful + List<GithubCommit> commits = middleware + .getAll(i -> ghApi + .getCommits(jwtHelper.getGhBearerString(installationId), apiVersion, repositoryFullName, pullRequestNumber), + GithubCommit.class); + LOGGER.trace("Retrieved {} commits for PR {} in repo {}", commits.size(), pullRequestNumber, repositoryUrl); + // set up the validation request from current data + return ValidationRequest + .builder() + .setProvider(ProviderType.GITHUB) + .setRepoUrl(URI.create(repositoryUrl)) + .setStrictMode(true) + .setCommits(commits + .stream() + .map(c -> Commit + .builder() + .setHash(c.getSha()) + .setAuthor(GitUser + .builder() + .setMail(c.getCommit().getAuthor().getEmail()) + .setName(c.getCommit().getAuthor().getName()) + .build()) + .setCommitter(GitUser + .builder() + .setMail(c.getCommit().getCommitter().getEmail()) + .setName(c.getCommit().getCommitter().getName()) + .build()) + .build()) + .collect(Collectors.toList())) + .build(); + } + + /** + * Attempts to retrieve a webhook tracking record given the installation, repository, and pull request number. + * + * @param installationId the installation ID for the ECA app in the given repository + * @param repositoryFullName the full repository name for the target repo, e.g. eclipse/jetty + * @param pullRequestNumber the pull request number that is being processed currently + * @return the webhook tracking record if it can be found, or an empty optional. + */ + Optional<GithubWebhookTracking> getExistingRequestInformation(String installationId, String repositoryFullName, int pullRequestNumber) { + checkRequestParameters(installationId, repositoryFullName, pullRequestNumber); + MultivaluedMap<String, String> params = new MultivaluedMapImpl<>(); + params.add(GitEcaParameterNames.INSTALLATION_ID_RAW, installationId); + params.add(GitEcaParameterNames.REPOSITORY_FULL_NAME_RAW, repositoryFullName); + params.add(GitEcaParameterNames.PULL_REQUEST_NUMBER_RAW, Integer.toString(pullRequestNumber)); + return dao.get(new RDBMSQuery<>(wrapper, filters.get(GithubWebhookTracking.class), params)).stream().findFirst(); + } + + /** + * Process the current request and update the checks state to pending then success or failure. Contains verbose TRACE + * logging for more info on the states of the validation for more information + * + * @param request information about the request from the GH webhook on what resource requested revalidation. Used to + * target the commit status of the resources + * @param vr the pseudo request generated from the contextual webhook data. Used to make use of existing validation + * logic. + * @return true if the validation passed, false otherwise. + */ + boolean handleGithubWebhookValidation(GithubWebhookRequest request, ValidationRequest vr) { + // update the status before processing + LOGGER + .trace(VALIDATION_LOGGING_MESSAGE, request.getRepository().getFullName(), request.getPullRequest().getNumber(), + GithubCommitStatuses.PENDING); + updateCommitStatus(request, GithubCommitStatuses.PENDING); + + // validate the response + LOGGER + .trace("Begining validation of request for {}/#{}", request.getRepository().getFullName(), + request.getPullRequest().getNumber()); + ValidationResponse r = validationService.validateIncomingRequest(vr, wrapper); + if (r.getPassed()) { + LOGGER + .trace(VALIDATION_LOGGING_MESSAGE, request.getRepository().getFullName(), request.getPullRequest().getNumber(), + GithubCommitStatuses.SUCCESS); + updateCommitStatus(request, GithubCommitStatuses.SUCCESS); + return true; + } + LOGGER + .trace(VALIDATION_LOGGING_MESSAGE, request.getRepository().getFullName(), request.getPullRequest().getNumber(), + GithubCommitStatuses.FAILURE); + updateCommitStatus(request, GithubCommitStatuses.FAILURE); + return false; + } + + /** + * Sends off a POST to update the commit status given a context for the current PR. + * + * @param request the current webhook update request + * @param state the state to set the status to + * @param fingerprint the internal unique string for the set of commits being processed + */ + private void updateCommitStatus(GithubWebhookRequest request, GithubCommitStatuses state) { + LOGGER + .trace("Generated access token for installation {}: {}", request.getInstallation().getId(), + jwtHelper.getGithubAccessToken(request.getInstallation().getId()).getToken()); + ghApi + .updateStatus(jwtHelper.getGhBearerString(request.getInstallation().getId()), apiVersion, + request.getRepository().getFullName(), request.getPullRequest().getHead().getSha(), + GithubCommitStatusRequest + .builder() + .setDescription(state.getMessage()) + .setState(state.toString()) + .setTargetUrl(serverTarget + "/git/eca/status/gh" + request.getRepository().getFullName() + '/' + + request.getPullRequest().getNumber()) + .setContext(context) + .build()); + } + + /** + * Validates required fields for processing requests. + * + * @param installationId the installation ID for the ECA app in the given repository + * @param repositoryFullName the full repository name for the target repo, e.g. eclipse/jetty + * @param pullRequestNumber the pull request number that is being processed currently + * @throws BadRequestException if at least one of the parameters is in an invalid state. + */ + private void checkRequestParameters(String installationId, String repositoryFullName, int pullRequestNumber) { + List<String> missingFields = new ArrayList<>(); + if (StringUtils.isBlank(installationId)) { + missingFields.add(GitEcaParameterNames.INSTALLATION_ID_RAW); + } + if (StringUtils.isBlank(repositoryFullName)) { + missingFields.add(GitEcaParameterNames.REPOSITORY_FULL_NAME_RAW); + } + if (pullRequestNumber < 1) { + missingFields.add(GitEcaParameterNames.PULL_REQUEST_NUMBER_RAW); + } + + // throw exception if some fields are missing as we can't continue to process the request + if (!missingFields.isEmpty()) { + throw new BadRequestException("Missing fields in order to prepare request: " + StringUtils.join(missingFields, ' ')); + } + } + +} diff --git a/src/main/java/org/eclipsefoundation/git/eca/resource/GithubWebhooksResource.java b/src/main/java/org/eclipsefoundation/git/eca/resource/GithubWebhooksResource.java index 088f365913b4be3a25a42d613f39cf1766b46e82..feb89850b30231d105dccb111d598ead6be19f81 100644 --- a/src/main/java/org/eclipsefoundation/git/eca/resource/GithubWebhooksResource.java +++ b/src/main/java/org/eclipsefoundation/git/eca/resource/GithubWebhooksResource.java @@ -15,7 +15,6 @@ import java.net.URI; import java.util.Arrays; import java.util.List; import java.util.Optional; -import java.util.stream.Collectors; import javax.inject.Inject; import javax.ws.rs.BadRequestException; @@ -23,38 +22,20 @@ import javax.ws.rs.FormParam; import javax.ws.rs.NotFoundException; import javax.ws.rs.POST; import javax.ws.rs.Path; -import javax.ws.rs.PathParam; -import javax.ws.rs.core.MultivaluedMap; +import javax.ws.rs.QueryParam; import javax.ws.rs.core.Response; -import org.eclipse.microprofile.config.inject.ConfigProperty; -import org.eclipse.microprofile.rest.client.inject.RestClient; import org.eclipsefoundation.core.helper.DateTimeHelper; -import org.eclipsefoundation.core.model.RequestWrapper; -import org.eclipsefoundation.core.service.APIMiddleware; import org.eclipsefoundation.core.service.CachingService; -import org.eclipsefoundation.git.eca.api.GithubAPI; -import org.eclipsefoundation.git.eca.api.models.GithubCommit; -import org.eclipsefoundation.git.eca.api.models.GithubCommitStatusRequest; import org.eclipsefoundation.git.eca.api.models.GithubWebhookRequest; import org.eclipsefoundation.git.eca.dto.GithubWebhookTracking; import org.eclipsefoundation.git.eca.helper.CaptchaHelper; -import org.eclipsefoundation.git.eca.helper.JwtHelper; -import org.eclipsefoundation.git.eca.model.Commit; -import org.eclipsefoundation.git.eca.model.GitUser; import org.eclipsefoundation.git.eca.model.RevalidationResponse; import org.eclipsefoundation.git.eca.model.ValidationRequest; -import org.eclipsefoundation.git.eca.model.ValidationResponse; import org.eclipsefoundation.git.eca.namespace.GitEcaParameterNames; -import org.eclipsefoundation.git.eca.namespace.GithubCommitStatuses; import org.eclipsefoundation.git.eca.namespace.HCaptchaErrorCodes; -import org.eclipsefoundation.git.eca.namespace.ProviderType; -import org.eclipsefoundation.git.eca.service.ValidationService; -import org.eclipsefoundation.persistence.dao.PersistenceDao; import org.eclipsefoundation.persistence.model.RDBMSQuery; -import org.eclipsefoundation.persistence.service.FilterService; import org.jboss.resteasy.annotations.jaxrs.HeaderParam; -import org.jboss.resteasy.specimpl.MultivaluedMapImpl; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -65,38 +46,14 @@ import org.slf4j.LoggerFactory; * */ @Path("webhooks/github") -public class GithubWebhooksResource { +public class GithubWebhooksResource extends GithubAdjacentResource { private static final Logger LOGGER = LoggerFactory.getLogger(GithubWebhooksResource.class); - private static final String VALIDATION_LOGGING_MESSAGE = "Setting validation state for {}/#{} to {}"; - - @ConfigProperty(name = "eclipse.webhooks.github.context") - String context; - @ConfigProperty(name = "eclipse.webhooks.github.server-target") - String serverTarget; - @ConfigProperty(name = "eclipse.github.default-api-version", defaultValue = "2022-11-28") - String apiVersion; - - @Inject - RequestWrapper wrapper; - @Inject - APIMiddleware middleware; - @Inject - ValidationService validationService; - @Inject - JwtHelper jwtHelper; @Inject CaptchaHelper captchaHelper; @Inject CachingService cache; - @Inject - PersistenceDao dao; - @Inject - FilterService filters; - - @RestClient - GithubAPI ghApi; /** * Entry point for processing Github webhook requests. Accepts standard fields as described in the <a href= @@ -120,11 +77,10 @@ public class GithubWebhooksResource { LOGGER.trace("Processing PR event for install {} with delivery ID of {}", request.getInstallation().getId(), deliveryId); // prepare for validation process by pre-processing into standard format ValidationRequest vr = generateRequest(request); - String fingerprint = validationService.generateRequestHash(vr); // track the request before we start processing - trackWebhookRequest(fingerprint, deliveryId, request); + trackWebhookRequest(deliveryId, request); // process the request - handleGithubWebhookValidation(request, vr, fingerprint); + handleGithubWebhookValidation(request, vr); return Response.ok().build(); } @@ -138,32 +94,36 @@ public class GithubWebhooksResource { * @return redirect to the pull request once done processing */ @POST - @Path("revalidate/{fingerprint}") - public Response revalidateWebhookRequest(@PathParam("fingerprint") String fingerprint, + @Path("revalidate") + public Response revalidateWebhookRequest(@QueryParam(GitEcaParameterNames.REPOSITORY_FULL_NAME_RAW) String fullRepoName, + @QueryParam(GitEcaParameterNames.INSTALLATION_ID_RAW) String installationId, + @QueryParam(GitEcaParameterNames.PULL_REQUEST_NUMBER_RAW) Integer prNo, @FormParam("h-captcha-response") String captchaResponse) { + // get the tracking if it exists + Optional<GithubWebhookTracking> optTracking = getExistingRequestInformation(installationId, fullRepoName, prNo); + if (optTracking.isEmpty()) { + throw new NotFoundException( + String.format("Cannot find a tracked pull request with for repo '%s', pull request number '%d'", fullRepoName, prNo)); + } + // check the captcha challenge response List<HCaptchaErrorCodes> errors = captchaHelper.validateCaptchaResponse(captchaResponse); if (!errors.isEmpty()) { // use debug logging as this could be incredibly noisy LOGGER - .debug("Captcha challenge failed with the following errors for request with fingerprint '{}': {}", fingerprint, - errors.stream().map(HCaptchaErrorCodes::getMessage)); + .debug("Captcha challenge failed with the following errors for revalidation request for '{}#{}': {}", fullRepoName, + prNo, errors.stream().map(HCaptchaErrorCodes::getMessage)); throw new BadRequestException("hCaptcha challenge response failed for this request"); } - Optional<GithubWebhookTracking> optTracking = findTrackedRequest(fingerprint); - if (optTracking.isEmpty()) { - throw new NotFoundException("Cannot find a tracked pull request with fingerprint " + fingerprint); - } - // get the tracking class, convert back to a GH webhook request, and validate the request GithubWebhookTracking tracking = optTracking.get(); GithubWebhookRequest request = GithubWebhookRequest.buildFromTracking(tracking); - boolean isSuccessful = handleGithubWebhookValidation(request, generateRequest(request), fingerprint); - LOGGER.debug("Revalidation for request with fingerprint '{}' was {}successful.", fingerprint, isSuccessful ? "" : " not"); + boolean isSuccessful = handleGithubWebhookValidation(request, generateRequest(request)); + LOGGER.debug("Revalidation for request for '{}#{}' was {}successful.", fullRepoName, prNo, isSuccessful ? "" : " not"); // update the tracking for the update time - trackWebhookRequest(fingerprint, tracking.getDeliveryId(), request); + trackWebhookRequest(tracking.getDeliveryId(), request); // build the url for pull request page StringBuilder sb = new StringBuilder(); sb.append("https://github.com/"); @@ -174,66 +134,6 @@ public class GithubWebhooksResource { return Response.ok(RevalidationResponse.builder().setLocation(URI.create(sb.toString())).build()).build(); } - /** - * Process the current request and update the checks state to pending then success or failure. Contains verbose TRACE - * logging for more info on the states of the validation for more information - * - * @param request information about the request from the GH webhook on what resource requested revalidation. Used to - * target the commit status of the resources - * @param vr the pseudo request generated from the contextual webhook data. Used to make use of existing validation - * logic. - * @param fingerprint the generated SHA hash for the request - * @return true if the validation passed, false otherwise. - */ - private boolean handleGithubWebhookValidation(GithubWebhookRequest request, ValidationRequest vr, String fingerprint) { - // update the status before processing - LOGGER - .trace(VALIDATION_LOGGING_MESSAGE, request.getRepository().getFullName(), request.getPullRequest().getNumber(), - GithubCommitStatuses.PENDING); - updateCommitStatus(request, GithubCommitStatuses.PENDING, fingerprint); - - // validate the response - LOGGER - .trace("Begining validation of request for {}/#{}", request.getRepository().getFullName(), - request.getPullRequest().getNumber()); - ValidationResponse r = validationService.validateIncomingRequest(vr, wrapper); - if (r.getPassed()) { - LOGGER - .trace(VALIDATION_LOGGING_MESSAGE, request.getRepository().getFullName(), request.getPullRequest().getNumber(), - GithubCommitStatuses.SUCCESS); - updateCommitStatus(request, GithubCommitStatuses.SUCCESS, fingerprint); - return true; - } - LOGGER - .trace(VALIDATION_LOGGING_MESSAGE, request.getRepository().getFullName(), request.getPullRequest().getNumber(), - GithubCommitStatuses.FAILURE); - updateCommitStatus(request, GithubCommitStatuses.FAILURE, fingerprint); - return false; - } - - /** - * Sends off a POST to update the commit status given a context for the current PR. - * - * @param request the current webhook update request - * @param state the state to set the status to - * @param fingerprint the internal unique string for the set of commits being processed - */ - private void updateCommitStatus(GithubWebhookRequest request, GithubCommitStatuses state, String fingerprint) { - LOGGER - .trace("Generated access token for installation {}: {}", request.getInstallation().getId(), - jwtHelper.getGithubAccessToken(request.getInstallation().getId()).getToken()); - ghApi - .updateStatus(jwtHelper.getGhBearerString(request.getInstallation().getId()), apiVersion, - request.getRepository().getFullName(), request.getPullRequest().getHead().getSha(), - GithubCommitStatusRequest - .builder() - .setDescription(state.getMessage()) - .setState(state.toString()) - .setTargetUrl(serverTarget + "/git/eca/status/" + fingerprint + "/ui") - .setContext(context) - .build()); - } - /** * Create a new entry in the webhook request tracking table to facilitate revalidation requests from the ECA system * rather than just from Github. @@ -243,10 +143,10 @@ public class GithubWebhooksResource { * @param request the webhook event payload from Github * @return the persisted webhook tracking data, or null if there was an error in creating the tracking */ - private GithubWebhookTracking trackWebhookRequest(String fingerprint, String deliveryId, GithubWebhookRequest request) { - Optional<GithubWebhookTracking> existingTracker = findTrackedRequest(fingerprint); + private GithubWebhookTracking trackWebhookRequest(String deliveryId, GithubWebhookRequest request) { + Optional<GithubWebhookTracking> existingTracker = getExistingRequestInformation(request.getInstallation().getId(), + request.getRepository().getFullName(), request.getPullRequest().getNumber()); GithubWebhookTracking calculatedTracker = new GithubWebhookTracking(); - calculatedTracker.setFingerprint(fingerprint); calculatedTracker.setInstallationId(request.getInstallation().getId()); calculatedTracker.setDeliveryId(deliveryId); calculatedTracker.setPullRequestNumber(request.getPullRequest().getNumber()); @@ -261,25 +161,13 @@ public class GithubWebhooksResource { .add(new RDBMSQuery<>(wrapper, filters.get(GithubWebhookTracking.class)), Arrays.asList(calculatedTracker)); if (results.isEmpty()) { LOGGER - .error("Could not save the webhook metadata, functionality will be restricted for request with fingerprint {}", - fingerprint); + .error("Could not save the webhook metadata, functionality will be restricted for request in repo '{}', pull request #{}", + calculatedTracker.getRepositoryFullName(), calculatedTracker.getPullRequestNumber()); return null; } return results.get(0); } - /** - * Retrieves the tracked information for a previous webhook validation request if available. - * - * @param fingerprint the unique hash for the request that was previously tracked - * @return an optional containing the tracked webhook request information, or an empty optional if it can't be found. - */ - private Optional<GithubWebhookTracking> findTrackedRequest(String fingerprint) { - MultivaluedMap<String, String> params = new MultivaluedMapImpl<>(); - params.add(GitEcaParameterNames.FINGERPRINT_RAW, fingerprint); - return dao.get(new RDBMSQuery<>(wrapper, filters.get(GithubWebhookTracking.class), params)).stream().findFirst(); - } - /** * Generate the validation request for the current GH Webhook request. * @@ -291,38 +179,4 @@ public class GithubWebhooksResource { request.getPullRequest().getNumber(), request.getRepository().getHtmlUrl()); } - private ValidationRequest generateRequest(String installationId, String repositoryFullName, int pullRequestNumber, - String repositoryUrl) { - // get the commits that will be validated, don't cache as changes can come in too fast for it to be useful - List<GithubCommit> commits = middleware - .getAll(i -> ghApi - .getCommits(jwtHelper.getGhBearerString(installationId), apiVersion, repositoryFullName, pullRequestNumber), - GithubCommit.class); - LOGGER.trace("Retrieved {} commits for PR {} in repo {}", commits.size(), pullRequestNumber, repositoryUrl); - // set up the validation request from current data - return ValidationRequest - .builder() - .setProvider(ProviderType.GITHUB) - .setRepoUrl(URI.create(repositoryUrl)) - .setStrictMode(true) - .setCommits(commits - .stream() - .map(c -> Commit - .builder() - .setHash(c.getSha()) - .setAuthor(GitUser - .builder() - .setMail(c.getCommit().getAuthor().getEmail()) - .setName(c.getCommit().getAuthor().getName()) - .build()) - .setCommitter(GitUser - .builder() - .setMail(c.getCommit().getCommitter().getEmail()) - .setName(c.getCommit().getCommitter().getName()) - .build()) - .build()) - .collect(Collectors.toList())) - .build(); - } - } diff --git a/src/main/java/org/eclipsefoundation/git/eca/resource/StatusResource.java b/src/main/java/org/eclipsefoundation/git/eca/resource/StatusResource.java new file mode 100644 index 0000000000000000000000000000000000000000..f71fdb02867ac5d41d7b525602d300b8921a14b1 --- /dev/null +++ b/src/main/java/org/eclipsefoundation/git/eca/resource/StatusResource.java @@ -0,0 +1,196 @@ +/** + * Copyright (c) 2023 Eclipse Foundation + * + * This program and the accompanying materials are made + * available under the terms of the Eclipse Public License 2.0 + * which is available at https://www.eclipse.org/legal/epl-2.0/ + * + * Author: Martin Lowe <martin.lowe@eclipse-foundation.org> + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.eclipsefoundation.git.eca.resource; + +import java.util.Arrays; +import java.util.List; +import java.util.Optional; +import java.util.stream.Collectors; + +import javax.inject.Inject; +import javax.ws.rs.BadRequestException; +import javax.ws.rs.GET; +import javax.ws.rs.NotFoundException; +import javax.ws.rs.Path; +import javax.ws.rs.PathParam; +import javax.ws.rs.Produces; +import javax.ws.rs.ServerErrorException; +import javax.ws.rs.core.MediaType; +import javax.ws.rs.core.Response; + +import org.apache.commons.lang3.StringUtils; +import org.eclipsefoundation.core.helper.DateTimeHelper; +import org.eclipsefoundation.core.service.CachingService; +import org.eclipsefoundation.git.eca.api.models.GithubWebhookRequest; +import org.eclipsefoundation.git.eca.api.models.GithubWebhookRequest.PullRequest; +import org.eclipsefoundation.git.eca.api.models.Project; +import org.eclipsefoundation.git.eca.dto.CommitValidationStatus; +import org.eclipsefoundation.git.eca.dto.GithubWebhookTracking; +import org.eclipsefoundation.git.eca.model.Commit; +import org.eclipsefoundation.git.eca.model.ValidationRequest; +import org.eclipsefoundation.git.eca.service.GithubApplicationService; +import org.eclipsefoundation.git.eca.service.ProjectsService; +import org.eclipsefoundation.git.eca.service.ValidationService; +import org.eclipsefoundation.persistence.model.RDBMSQuery; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import io.quarkus.qute.Location; +import io.quarkus.qute.Template; + +/** + * REST resource containing endpoints related to checking the status of validation requests. + * + * @author Martin Lowe + * + */ +@Path("eca/status") +public class StatusResource extends GithubAdjacentResource { + private static final Logger LOGGER = LoggerFactory.getLogger(StatusResource.class); + + @Inject + CachingService cache; + + @Inject + ProjectsService projects; + @Inject + ValidationService validation; + @Inject + GithubApplicationService ghAppService; + + // Qute templates, generates UI status page + @Location("simple_fingerprint_ui") + Template statusUiTemplate; + + @GET + @Path("{fingerprint}") + public Response getCommitValidation(@PathParam("fingerprint") String fingerprint) { + return Response.ok(validation.getHistoricValidationStatus(wrapper, fingerprint)).build(); + } + + @GET + @Produces(MediaType.TEXT_HTML) + @Path("{fingerprint}/ui") + public Response getCommitValidationUI(@PathParam("fingerprint") String fingerprint) { + List<CommitValidationStatus> statuses = validation.getHistoricValidationStatus(wrapper, fingerprint); + if (statuses.isEmpty()) { + return Response.status(404).build(); + } + List<Project> ps = projects.retrieveProjectsForRepoURL(statuses.get(0).getRepoUrl(), statuses.get(0).getProvider()); + return Response + .ok() + .entity(statusUiTemplate + .data("statuses", statuses) + .data("pullRequestNumber", null) + .data("fullRepoName", null) + .data("project", ps.isEmpty() ? null : ps.get(0)) + .data("repoUrl", statuses.get(0).getRepoUrl()) + .data("installationId", null) + .render()) + .build(); + } + + @GET + @Produces(MediaType.TEXT_HTML) + @Path("gh/{org}/{repoName}/{prNo}") + public Response getCommitValidationForGithub(@PathParam("org") String org, @PathParam("repoName") String repoName, + @PathParam("prNo") Integer prNo) { + String fullRepoName = org + '/' + repoName; + // get the installation ID for the given repo if it exists, and if the PR noted exists + String installationId = ghAppService.getInstallationForRepo(fullRepoName); + Optional<PullRequest> prResponse = ghAppService.getPullRequest(installationId, fullRepoName, prNo); + if (StringUtils.isBlank(installationId)) { + throw new BadRequestException("Could not find an ECA app installation for repo name: " + fullRepoName); + } else if (prResponse.isEmpty() || !"open".equalsIgnoreCase(prResponse.get().getState())) { + throw new NotFoundException(String.format("Could not find open PR '%d' in repo name '%s'", prNo, fullRepoName)); + } + + // prepare the request for consumption + String repoUrl = "https://github.com/" + fullRepoName; + ValidationRequest vr = generateRequest(installationId, fullRepoName, prNo, repoUrl); + // build the commit sha list based on the prepared request + List<String> commitShas = vr.getCommits().stream().map(Commit::getHash).collect(Collectors.toList()); + // there should always be commits for a PR, but in case, lets check + if (commitShas.isEmpty()) { + throw new BadRequestException(String.format("Could not find any commits for %s#%d", fullRepoName, prNo)); + } + LOGGER.debug("Found {} commits for '{}#{}'", commitShas.size(), fullRepoName, prNo); + + // get the commit status of commits to use + List<CommitValidationStatus> statuses = validation.getHistoricValidationStatusByShas(wrapper, commitShas); + // check if this request has been validated in the past, and if not, run validation + Optional<GithubWebhookTracking> tracking = getExistingRequestInformation(installationId, fullRepoName, prNo); + if (tracking.isEmpty() || commitShas.size() != statuses.size()) { + LOGGER.debug("Validation for {}#{} does not seem to be current, revalidating commits", fullRepoName, prNo); + // update the tracking before revalidating the request + GithubWebhookTracking updatedTracking = updateGithubTrackingIfMissing(tracking, prResponse.get(), installationId, fullRepoName); + if (updatedTracking == null) { + throw new ServerErrorException("Error while attempting to revalidate request, try again later.", 500); + } + // using the updated tracking, perform the validation + handleGithubWebhookValidation(GithubWebhookRequest.buildFromTracking(updatedTracking), vr); + // call to retrieve the statuses once again since they will have changed at this point + statuses = validation.getHistoricValidationStatusByShas(wrapper, commitShas); + } + + // get projects for use in status UI + List<Project> ps = projects.retrieveProjectsForRepoURL(statuses.get(0).getRepoUrl(), statuses.get(0).getProvider()); + // render and return the status UI + return Response + .ok() + .entity(statusUiTemplate + .data("statuses", statuses) + .data("pullRequestNumber", prNo) + .data("fullRepoName", fullRepoName) + .data("project", ps.isEmpty() ? null : ps.get(0)) + .data("repoUrl", repoUrl) + .data("installationId", installationId) + .render()) + .build(); + } + + /** + * Checks if the Github tracking is present for the current request, and if missing will generate a new record and save + * it. + * + * @param tracking the optional tracking entry for the current request + * @param installationId the ECA app installation ID for the current request + * @param request the pull request that is being validated + * @param fullRepoName the full repo name for the validation request + */ + private GithubWebhookTracking updateGithubTrackingIfMissing(Optional<GithubWebhookTracking> tracking, PullRequest request, + String installationId, String fullRepoName) { + // if there is no tracking present, create the missing tracking and persist it + if (tracking.isEmpty()) { + GithubWebhookTracking newTracking = new GithubWebhookTracking(); + newTracking.setDeliveryId(""); + newTracking.setFingerprint(""); + newTracking.setHeadSha(request.getHead().getSha()); + newTracking.setInstallationId(installationId); + newTracking.setLastUpdated(DateTimeHelper.now()); + newTracking.setPullRequestNumber(request.getNumber()); + newTracking.setRepositoryFullName(fullRepoName); + + // save the data, and log on its success or failure + List<GithubWebhookTracking> savedTracking = dao + .add(new RDBMSQuery<>(wrapper, filters.get(GithubWebhookTracking.class)), Arrays.asList(newTracking)); + if (savedTracking.isEmpty()) { + LOGGER.warn("Unable to create new GH tracking record for request to validate {}#{}", fullRepoName, request.getNumber()); + return null; + } + // return the updated tracking when successful + LOGGER.debug("Created new GH tracking record for request to validate {}#{}", fullRepoName, request.getNumber()); + return savedTracking.get(0); + } + return tracking.get(); + } +} diff --git a/src/main/java/org/eclipsefoundation/git/eca/resource/ValidationResource.java b/src/main/java/org/eclipsefoundation/git/eca/resource/ValidationResource.java index 98b58b95568007af7639c90fd8548821517e26ea..90df077eab27d987906b83d80308fa98d3aae66c 100644 --- a/src/main/java/org/eclipsefoundation/git/eca/resource/ValidationResource.java +++ b/src/main/java/org/eclipsefoundation/git/eca/resource/ValidationResource.java @@ -12,7 +12,6 @@ **********************************************************************/ package org.eclipsefoundation.git.eca.resource; -import java.net.MalformedURLException; import java.util.ArrayList; import java.util.List; import java.util.Objects; @@ -22,7 +21,6 @@ import javax.ws.rs.Consumes; import javax.ws.rs.GET; import javax.ws.rs.POST; import javax.ws.rs.Path; -import javax.ws.rs.PathParam; import javax.ws.rs.Produces; import javax.ws.rs.core.MediaType; import javax.ws.rs.core.Response; @@ -31,8 +29,6 @@ import org.eclipse.microprofile.config.inject.ConfigProperty; import org.eclipsefoundation.core.model.RequestWrapper; import org.eclipsefoundation.core.service.CachingService; import org.eclipsefoundation.git.eca.api.models.EclipseUser; -import org.eclipsefoundation.git.eca.api.models.Project; -import org.eclipsefoundation.git.eca.dto.CommitValidationStatus; import org.eclipsefoundation.git.eca.model.ValidationRequest; import org.eclipsefoundation.git.eca.model.ValidationResponse; import org.eclipsefoundation.git.eca.namespace.APIStatusCode; @@ -44,9 +40,6 @@ import org.jboss.resteasy.annotations.jaxrs.QueryParam; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import io.quarkus.qute.Location; -import io.quarkus.qute.Template; - /** * ECA validation endpoint for Git commits. Will use information from the bots, projects, and accounts API to validate * commits passed to this endpoint. Should be as system agnostic as possible to allow for any service to request @@ -82,10 +75,6 @@ public class ValidationResource { @Inject InterestGroupService ig; - // Qute templates, generates email bodies - @Location("simple_fingerprint_ui") - Template membershipTemplateWeb; - /** * Consuming a JSON request, this method will validate all passed commits, using the repo URL and the repository * provider. These commits will be validated to ensure that all users are covered either by an ECA, or are committers on @@ -95,7 +84,6 @@ public class ValidationResource { * @param req the request containing basic data plus the commits to be validated * @return a web response indicating success or failure for each commit, along with standard messages that may be used * to give users context on failure. - * @throws MalformedURLException */ @POST public Response validate(ValidationRequest req) { @@ -112,30 +100,6 @@ public class ValidationResource { } } - @GET - @Path("status/{fingerprint}") - public Response getCommitValidation(@PathParam("fingerprint") String fingerprint) { - return Response.ok(validation.getHistoricValidationStatus(wrapper, fingerprint)).build(); - } - - @GET - @Produces(MediaType.TEXT_HTML) - @Path("status/{fingerprint}/ui") - public Response getCommitValidationUI(@PathParam("fingerprint") String fingerprint) { - List<CommitValidationStatus> statuses = validation.getHistoricValidationStatus(wrapper, fingerprint); - if (statuses.isEmpty()) { - return Response.status(404).build(); - } - List<Project> ps = projects.retrieveProjectsForRepoURL(statuses.get(0).getRepoUrl(), statuses.get(0).getProvider()); - return Response - .ok() - .entity(membershipTemplateWeb - .data("statuses", statuses, "repoUrl", statuses.get(0).getRepoUrl(), "project", ps.isEmpty() ? null : ps.get(0), - "fingerprint", fingerprint) - .render()) - .build(); - } - @GET @Path("/lookup") public Response getUserStatus(@QueryParam("email") String email) { diff --git a/src/main/java/org/eclipsefoundation/git/eca/service/GithubApplicationService.java b/src/main/java/org/eclipsefoundation/git/eca/service/GithubApplicationService.java new file mode 100644 index 0000000000000000000000000000000000000000..3a36f46cafccdc234ec3197048ece07b7cf86ec9 --- /dev/null +++ b/src/main/java/org/eclipsefoundation/git/eca/service/GithubApplicationService.java @@ -0,0 +1,43 @@ +/** + * Copyright (c) 2023 Eclipse Foundation + * + * This program and the accompanying materials are made + * available under the terms of the Eclipse Public License 2.0 + * which is available at https://www.eclipse.org/legal/epl-2.0/ + * + * Author: Martin Lowe <martin.lowe@eclipse-foundation.org> + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.eclipsefoundation.git.eca.service; + +import java.util.Optional; + +import org.eclipsefoundation.git.eca.api.models.GithubWebhookRequest.PullRequest; + +/** + * Service for interacting with the Github API with caching for performance. + * + * @author Martin Lowe + * + */ +public interface GithubApplicationService { + + /** + * Retrieves the installation ID for the ECA app on the given repo if it exists. + * + * @param repoFullName the full repo name to retrieve an installation ID for. E.g. eclipse/jetty + * @return the numeric installation ID if it exists, or null + */ + String getInstallationForRepo(String repoFullName); + + /** + * Retrieves a pull request given the repo, pull request, and associated installation to action the fetch. + * + * @param installationId installation ID to use when creating access tokens to query GH API + * @param repoFullName the full repo name, where the org and repo name are joined by a slash, e.g. eclipse/jetty + * @param pullRequest the pull request numeric ID + * @return the pull request if it exists, otherwise empty + */ + Optional<PullRequest> getPullRequest(String installationId, String repoFullName, Integer pullRequest); +} diff --git a/src/main/java/org/eclipsefoundation/git/eca/service/ValidationService.java b/src/main/java/org/eclipsefoundation/git/eca/service/ValidationService.java index 0110228cebf0d2544c337cd6a2614f798fdf6836..1f441ca0b783c220d831a8661b0734c336dbf081 100644 --- a/src/main/java/org/eclipsefoundation/git/eca/service/ValidationService.java +++ b/src/main/java/org/eclipsefoundation/git/eca/service/ValidationService.java @@ -50,6 +50,15 @@ public interface ValidationService { */ public List<CommitValidationStatus> getHistoricValidationStatus(RequestWrapper wrapper, String fingerprint); + /** + * Retrieves a set of validation status objects given the target shas. + * + * @param wrapper current request wrapper object + * @param shas list of shas to use when fetching historic commit statuses + * @return the list of historic validation status objects, or an empty list. + */ + public List<CommitValidationStatus> getHistoricValidationStatusByShas(RequestWrapper wrapper, List<String> shas); + /** * Retrieves a set of commit validation status objects given a validation request and target project. * diff --git a/src/main/java/org/eclipsefoundation/git/eca/service/impl/DefaultGithubApplicationService.java b/src/main/java/org/eclipsefoundation/git/eca/service/impl/DefaultGithubApplicationService.java new file mode 100644 index 0000000000000000000000000000000000000000..9027ca48dbe1a08ad8472ea7ee1ed563d41df7b0 --- /dev/null +++ b/src/main/java/org/eclipsefoundation/git/eca/service/impl/DefaultGithubApplicationService.java @@ -0,0 +1,144 @@ +/** + * Copyright (c) 2023 Eclipse Foundation + * + * This program and the accompanying materials are made + * available under the terms of the Eclipse Public License 2.0 + * which is available at https://www.eclipse.org/legal/epl-2.0/ + * + * Author: Martin Lowe <martin.lowe@eclipse-foundation.org> + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.eclipsefoundation.git.eca.service.impl; + +import java.time.Duration; +import java.util.List; +import java.util.Optional; +import java.util.concurrent.TimeUnit; + +import javax.annotation.PostConstruct; +import javax.enterprise.context.ApplicationScoped; +import javax.inject.Inject; +import javax.ws.rs.core.MultivaluedMap; + +import org.eclipse.microprofile.config.inject.ConfigProperty; +import org.eclipse.microprofile.context.ManagedExecutor; +import org.eclipse.microprofile.rest.client.inject.RestClient; +import org.eclipsefoundation.core.service.APIMiddleware; +import org.eclipsefoundation.core.service.CachingService; +import org.eclipsefoundation.git.eca.api.GithubAPI; +import org.eclipsefoundation.git.eca.api.models.GithubApplicationInstallation; +import org.eclipsefoundation.git.eca.api.models.GithubInstallationRepositoriesResponse; +import org.eclipsefoundation.git.eca.api.models.GithubWebhookRequest.PullRequest; +import org.eclipsefoundation.git.eca.helper.JwtHelper; +import org.eclipsefoundation.git.eca.service.GithubApplicationService; +import org.jboss.resteasy.specimpl.MultivaluedMapImpl; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.github.benmanes.caffeine.cache.AsyncLoadingCache; +import com.github.benmanes.caffeine.cache.Caffeine; + +/** + * Default caching implementation of the GH app service. This uses a loading cache to keep installation info highly + * available to reduce latency in calls. + * + * @author Martin Lowe + * + */ +@ApplicationScoped +public class DefaultGithubApplicationService implements GithubApplicationService { + private static final Logger LOGGER = LoggerFactory.getLogger(DefaultGithubApplicationService.class); + + @ConfigProperty(name = "eclipse.github.default-api-version", defaultValue = "2022-11-28") + String apiVersion; + + @RestClient + GithubAPI gh; + + @Inject + JwtHelper jwt; + @Inject + CachingService cache; + @Inject + APIMiddleware middle; + + @Inject + ManagedExecutor exec; + + private AsyncLoadingCache<String, MultivaluedMap<String, String>> installationRepositoriesCache; + + @PostConstruct + void init() { + this.installationRepositoriesCache = Caffeine + .newBuilder() + .executor(exec) + .maximumSize(10) + .refreshAfterWrite(Duration.ofMinutes(60)) + .buildAsync(k -> loadInstallationRepositories()); + // do initial map population + getAllInstallRepos(); + } + + @Override + public String getInstallationForRepo(String repoFullName) { + MultivaluedMap<String, String> map = getAllInstallRepos(); + return map.keySet().stream().filter(k -> map.get(k).contains(repoFullName)).findFirst().orElse(null); + } + + private MultivaluedMap<String, String> getAllInstallRepos() { + try { + return this.installationRepositoriesCache.get("all").get(10L, TimeUnit.SECONDS); + } catch (InterruptedException e) { + LOGGER.error("Thread interrupted while building repository cache, no entries will be available for current call"); + Thread.currentThread().interrupt(); + } catch (Exception e) { + // rewrap exception and throw + throw new RuntimeException(e); + } + return new MultivaluedMapImpl<>(); + } + + @Override + public Optional<PullRequest> getPullRequest(String installationId, String repoFullName, Integer pullRequest) { + return cache + .get(repoFullName, new MultivaluedMapImpl<>(), PullRequest.class, + () -> gh.getPullRequest(jwt.getGhBearerString(installationId), apiVersion, repoFullName, pullRequest)); + } + + /** + * Retrieves a fresh copy of installation repositories, mapped by installation ID to associated full repo names. + * + * @return a multivalued map relating installation IDs to associated full repo names. + */ + private MultivaluedMap<String, String> loadInstallationRepositories() { + // create map early for potential empty returns + MultivaluedMapImpl<String, String> out = new MultivaluedMapImpl<>(); + // get JWT bearer and then get all associated installations of current app + String auth = "Bearer " + jwt.generateJwt(); + List<GithubApplicationInstallation> installations = middle + .getAll(i -> gh.getInstallations(i, auth), GithubApplicationInstallation.class); + // check that there are installations + if (installations.isEmpty()) { + LOGGER.warn("Did not find any installations for the currently configured Github application"); + return out; + } + // trace log the installations for more context + LOGGER.trace("Found the following installations: {}", installations); + + // from installations, get the assoc. repos and grab their full repo name and collect them + installations + .stream() + .forEach(installation -> middle + .getAll(i -> gh.getInstallationRepositories(i, jwt.getGhBearerString(Integer.toString(installation.getId()))), + GithubInstallationRepositoriesResponse.class) + .stream() + .forEach(installRepo -> installRepo + .getRepositories() + .stream() + .forEach(r -> out.add(Integer.toString(installation.getId()), r.getFullName())))); + LOGGER.trace("Final results for generating installation mapping: {}", out); + return out; + } + +} diff --git a/src/main/java/org/eclipsefoundation/git/eca/service/impl/DefaultValidationService.java b/src/main/java/org/eclipsefoundation/git/eca/service/impl/DefaultValidationService.java index 76d319e7d6b86a95c2307fada3e8fdc6b67e7431..a9fa1872f942f0cce6eeeed421a92dd67729fc19 100644 --- a/src/main/java/org/eclipsefoundation/git/eca/service/impl/DefaultValidationService.java +++ b/src/main/java/org/eclipsefoundation/git/eca/service/impl/DefaultValidationService.java @@ -15,6 +15,7 @@ import java.util.ArrayList; import java.util.Collections; import java.util.List; import java.util.Optional; +import java.util.concurrent.TimeUnit; import java.util.stream.Collectors; import javax.enterprise.context.ApplicationScoped; @@ -112,7 +113,7 @@ public class DefaultValidationService implements ValidationService { // get the current status if present Optional<CommitValidationStatus> status = statuses.stream().filter(s -> s.getCommitHash().equals(c.getHash())).findFirst(); // skip the commit validation if already passed - if (status.isPresent() && status.get().getErrors().isEmpty()) { + if (isValidationStatusCurrentAndValid(status, c)) { r.addMessage(c.getHash(), "Commit was previously validated, skipping processing", APIStatusCode.SUCCESS_SKIPPED); return; } @@ -136,6 +137,19 @@ public class DefaultValidationService implements ValidationService { return dao.get(q).stream().map(statusGrouping -> statusGrouping.getCompositeId().getCommit()).collect(Collectors.toList()); } + @Override + public List<CommitValidationStatus> getHistoricValidationStatusByShas(RequestWrapper wrapper, List<String> shas) { + if (shas == null || shas.isEmpty()) { + return Collections.emptyList(); + } + MultivaluedMap<String, String> params = new MultivaluedMapImpl<>(); + params.put(GitEcaParameterNames.SHAS_RAW, shas); + RDBMSQuery<CommitValidationStatus> q = new RDBMSQuery<>(wrapper, filters.get(CommitValidationStatus.class), params); + // set use limit to false to collect all data in one request + q.setUseLimit(false); + return dao.get(q); + } + @Override public List<CommitValidationStatus> getRequestCommitValidationStatus(RequestWrapper wrapper, ValidationRequest req, String projectId) { RDBMSQuery<CommitValidationStatus> q = new RDBMSQuery<>(wrapper, filters.get(CommitValidationStatus.class), @@ -151,6 +165,15 @@ public class DefaultValidationService implements ValidationService { // iterate over commit responses, and update statuses in DB List<CommitValidationStatus> updatedStatuses = new ArrayList<>(); r.getCommits().entrySet().stream().filter(e -> !ValidationResponse.NIL_HASH_PLACEHOLDER.equalsIgnoreCase(e.getKey())).forEach(e -> { + // get the commit for current status + Optional<Commit> commit = req.getCommits().stream().filter(c -> e.getKey().equals(c.getHash())).findFirst(); + if (commit.isEmpty()) { + // this should always have a match (response commits are built from request commits) + LOGGER.error("Could not find request commit associated with commit messages for commit hash '{}'", e.getKey()); + return; + } + Commit c = commit.get(); + // update the status if present, otherwise make new one. Optional<CommitValidationStatus> status = statuses.stream().filter(s -> e.getKey().equals(s.getCommitHash())).findFirst(); CommitValidationStatus base; @@ -160,6 +183,7 @@ public class DefaultValidationService implements ValidationService { base = new CommitValidationStatus(); base.setProject(CommitHelper.getProjectId(p)); base.setCommitHash(e.getKey()); + base.setUserMail(c.getAuthor().getMail()); base.setProvider(req.getProvider()); base.setRepoUrl(req.getRepoUrl().toString()); base.setCreationDate(DateTimeHelper.now()); @@ -167,13 +191,7 @@ public class DefaultValidationService implements ValidationService { } base.setLastModified(DateTimeHelper.now()); updatedStatuses.add(base); - // get the commit for current status - Optional<Commit> commit = req.getCommits().stream().filter(c -> e.getKey().equals(c.getHash())).findFirst(); - if (commit.isEmpty()) { - LOGGER.error("Could not find request commit associated with commit messages for commit hash '{}'", e.getKey()); - return; - } - Commit c = commit.get(); + // if there are errors, update validation messages if (!e.getValue().getErrors().isEmpty() || (base.getErrors() != null && !base.getErrors().isEmpty())) { // generate new errors, looking for errors not found in current list @@ -234,7 +252,7 @@ public class DefaultValidationService implements ValidationService { response.addMessage(c.getHash(), String.format("Authored by: %1$s <%2$s>", author.getName(), author.getMail())); // skip processing if a merge commit - if (c.getParents().size() > 1) { + if (c.getParents() != null && c.getParents().size() > 1) { response .addMessage(c.getHash(), String.format("Commit '%1$s' has multiple parents, merge commit detected, passing", c.getHash())); @@ -434,4 +452,27 @@ public class DefaultValidationService implements ValidationService { } return null; } + + /** + * <p> + * Checks the following to determine whether a commit has already been validated: + * </p> + * <ul> + * <li>The validation status exists + * <li>Validation status has no present errors + * <li>Modification date is either unset or matches when set + * <li>User mail is set and matches (ignores case) + * </ul> + * + * If any of these checks fail, then the commit should be revalidated. + * + * @param status the current commits validation status if it exists + * @param c the commit that is being validated + * @return true if the commit does not need to be (re)validated, false otherwise. + */ + private boolean isValidationStatusCurrentAndValid(Optional<CommitValidationStatus> status, Commit c) { + return status.isPresent() && status.get().getErrors().isEmpty() && c.getAuthor() != null + && status.get().getUserMail().equalsIgnoreCase(c.getAuthor().getMail()) + && (c.getLastModificationDate() == null || status.get().getLastModified().equals(c.getLastModificationDate())); + } } diff --git a/src/main/resources/templates/simple_fingerprint_ui.html b/src/main/resources/templates/simple_fingerprint_ui.html index 69af38b850d58c315e8dc24f8ecabcb003e97bf3..f0eb2bea4a13405b6465d80c7d7994b245eaa32a 100644 --- a/src/main/resources/templates/simple_fingerprint_ui.html +++ b/src/main/resources/templates/simple_fingerprint_ui.html @@ -140,9 +140,9 @@ </div> </div> {/if} - {#if statuses.0.provider == ProviderType:GITHUB} + {#if statuses.0.provider == ProviderType:GITHUB && pullRequestNumber != null && fullRepoName != null} <div> - <form id="git-eca-hook-revalidation" data-request-id="{fingerprint}"> + <form id="git-eca-hook-revalidation" data-request-number="{pullRequestNumber}" data-request-repo="{fullRepoName}" data-request-installation="{installationId}"> <div class="captcha"> <div class="h-captcha" data-sitekey="{config:['eclipse.hcaptcha.sitekey']}"></div> </div> @@ -266,9 +266,15 @@ const $submitButton = $form.find('button'); // disable the button so that requests won't be spammed $submitButton.attr("disabled", "disabled"); + // set up params to set up for revalidation + let params = $.param({ + repo_full_name: $form.data('request-repo'), + pull_request_number: $form.data('request-number'), + installation_id: $form.data('request-installation') + }); // use ajax to revalidate the commit with GH $.ajax({ - url: `/git/webhooks/github/revalidate/${$form.data('request-id')}`, + url: `/git/webhooks/github/revalidate?${params}`, data: $form.serialize(), type: 'POST', success: function (data) { diff --git a/src/test/java/org/eclipsefoundation/git/eca/helper/CommitHelperTest.java b/src/test/java/org/eclipsefoundation/git/eca/helper/CommitHelperTest.java index bb24b9eb6b69cfd6d37228c08a135d192e2148d6..982eb6ae3e9e2a1b0f6799bb4e3b5fa0d9e74dab 100644 --- a/src/test/java/org/eclipsefoundation/git/eca/helper/CommitHelperTest.java +++ b/src/test/java/org/eclipsefoundation/git/eca/helper/CommitHelperTest.java @@ -58,13 +58,6 @@ class CommitHelperTest { Assertions.assertFalse(CommitHelper.validateCommit(null), "Expected null commit to fail validation"); } - @Test - void validateCommitNoAuthor() { - baseCommit.setAuthor(null); - Assertions.assertFalse(CommitHelper.validateCommit(baseCommit.build()), - "Expected basic commit to fail validation w/ no author"); - } - @Test void validateCommitNoAuthorMail() { baseCommit.setAuthor(GitUser.builder().setName("Some Name").build()); @@ -72,13 +65,6 @@ class CommitHelperTest { "Expected basic commit to fail validation w/ no author mail address"); } - @Test - void validateCommitNoCommitter() { - baseCommit.setCommitter(null); - Assertions.assertFalse(CommitHelper.validateCommit(baseCommit.build()), - "Expected basic commit to fail validation w/ no committer"); - } - @Test void validateCommitNoCommitterMail() { baseCommit.setCommitter(GitUser.builder().setName("Some Name").build()); @@ -86,13 +72,6 @@ class CommitHelperTest { "Expected basic commit to fail validation w/ no committer mail address"); } - @Test - void validateCommitNoHash() { - baseCommit.setHash(null); - Assertions.assertFalse(CommitHelper.validateCommit(baseCommit.build()), - "Expected basic commit to fail validation w/ no commit hash"); - } - @Test void validateCommitNoBody() { baseCommit.setBody(null); diff --git a/src/test/resources/database/default/V1.0.0__default.sql b/src/test/resources/database/default/V1.0.0__default.sql index 1f941ac350a9b30358da165ec1d11e50b8bb22db..ff4c33f79679e8b5cf09f6bfaa19a91d37d6827a 100644 --- a/src/test/resources/database/default/V1.0.0__default.sql +++ b/src/test/resources/database/default/V1.0.0__default.sql @@ -7,6 +7,7 @@ CREATE TABLE CommitValidationStatus ( provider varchar(100) NOT NULL, estimatedLoc int DEFAULT 0, repoUrl varchar(255) NOT NULL, + userMail varchar(255) DEFAULT NULL, PRIMARY KEY (id) ); INSERT INTO CommitValidationStatus(commitHash,project,lastModified,creationDate,provider, repoUrl) VALUES('123456789', 'sample.proj', NOW(), NOW(),'GITHUB','http://www.github.com/eclipsefdn/sample');