Skip to content
Snippets Groups Projects
Commit ec3d28f0 authored by Martin Lowe's avatar Martin Lowe :flag_ca:
Browse files

Add new status URL for GH, remove fingerprint logic from GH webhook code

To better target data and remove some outdated tracking methods, the
previous fingerprint mechanism has been removed from the GH webhook
code. Additionally, a new status URL matching the format
git/eca/status/gh/{fullRepoUrl}/{prNumber} has been added to remove the
need for a fingerprint.
parent f3702dc8
No related branches found
No related tags found
No related merge requests found
Pipeline #16344 passed
Showing
with 766 additions and 180 deletions
......@@ -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,6 +21,7 @@ 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.jboss.resteasy.util.HttpHeaderNames;
......@@ -34,6 +36,12 @@ import org.jboss.resteasy.util.HttpHeaderNames;
@Produces("application/json")
public interface GithubAPI {
@GET
@Path("repos/{repoFull}/pulls/{pullNumber}")
public Response getPullRequest(@HeaderParam(HttpHeaderNames.AUTHORIZATION) String bearer,
@HeaderParam("X-GitHub-Api-Version") String apiVersion, @PathParam("repoFull") String repoFull,
@PathParam("pullNumber") int pullNumber);
@GET
@Path("repos/{repoFull}/pulls/{pullNumber}/commits")
public Response getCommits(@HeaderParam(HttpHeaderNames.AUTHORIZATION) String bearer,
......@@ -42,11 +50,29 @@ public interface GithubAPI {
@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 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);
@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);
@GET
@Path("installation/repositories")
public Response getInstallationRepositories(@BeanParam BaseAPIParameters params,
@HeaderParam(HttpHeaderNames.AUTHORIZATION) String bearer);
}
/**
* 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 current
*
* @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();
}
}
/**
* 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();
}
}
......@@ -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;
}
......
......@@ -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;
}
......
......@@ -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));
}
......
......@@ -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();
}
}
......@@ -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);
}
}
/**
* 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.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.namespace.GitEcaParameterNames;
import org.eclipsefoundation.git.eca.namespace.ProviderType;
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);
@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
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();
}
/**
* 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, ' '));
}
}
}
......@@ -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,25 @@ 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,7 +51,7 @@ 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 {}";
......@@ -74,29 +60,14 @@ public class GithubWebhooksResource {
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 +91,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 +108,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/");
......@@ -182,15 +156,14 @@ public class GithubWebhooksResource {
* 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) {
private 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, fingerprint);
updateCommitStatus(request, GithubCommitStatuses.PENDING);
// validate the response
LOGGER
......@@ -201,13 +174,13 @@ public class GithubWebhooksResource {
LOGGER
.trace(VALIDATION_LOGGING_MESSAGE, request.getRepository().getFullName(), request.getPullRequest().getNumber(),
GithubCommitStatuses.SUCCESS);
updateCommitStatus(request, GithubCommitStatuses.SUCCESS, fingerprint);
updateCommitStatus(request, GithubCommitStatuses.SUCCESS);
return true;
}
LOGGER
.trace(VALIDATION_LOGGING_MESSAGE, request.getRepository().getFullName(), request.getPullRequest().getNumber(),
GithubCommitStatuses.FAILURE);
updateCommitStatus(request, GithubCommitStatuses.FAILURE, fingerprint);
updateCommitStatus(request, GithubCommitStatuses.FAILURE);
return false;
}
......@@ -218,7 +191,7 @@ public class GithubWebhooksResource {
* @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) {
private void updateCommitStatus(GithubWebhookRequest request, GithubCommitStatuses state) {
LOGGER
.trace("Generated access token for installation {}: {}", request.getInstallation().getId(),
jwtHelper.getGithubAccessToken(request.getInstallation().getId()).getToken());
......@@ -229,7 +202,8 @@ public class GithubWebhooksResource {
.builder()
.setDescription(state.getMessage())
.setState(state.toString())
.setTargetUrl(serverTarget + "/git/eca/status/" + fingerprint + "/ui")
.setTargetUrl(serverTarget + "/git/eca/status/gh" + request.getRepository().getFullName() + '/'
+ request.getPullRequest().getNumber())
.setContext(context)
.build());
}
......@@ -243,10 +217,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 +235,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 +253,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();
}
}
/**
* 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.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.core.MediaType;
import javax.ws.rs.core.Response;
import org.apache.commons.lang3.StringUtils;
import org.eclipsefoundation.core.service.CachingService;
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.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, "repoUrl", statuses.get(0).getRepoUrl(), "project", ps.isEmpty() ? null : ps.get(0))
.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);
if (StringUtils.isBlank(installationId)) {
throw new BadRequestException("Could not find an ECA app installation for repo name: " + fullRepoName);
} else if (!ghAppService.doesPullRequestExist(installationId, fullRepoName, prNo)) {
throw new NotFoundException(String.format("Could not find 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);
// process the incoming request to ensure all commits have been validated (successful or not)
validation.validateIncomingRequest(vr, wrapper);
}
// 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();
}
}
/**
* 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;
/**
* @author martin
*
*/
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);
/**
*
* @param installationId
* @param repoFullName
* @param pullRequest
* @return
*/
boolean doesPullRequestExist(String installationId, String repoFullName, Integer pullRequest);
}
......@@ -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.
*
......
/**
* 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 javax.ws.rs.core.Response;
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.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 boolean doesPullRequestExist(String installationId, String repoFullName, Integer pullRequest) {
// get a cached response to check if the PR exists (indicated by a 200 response)
Optional<Response> prResponse = cache
.get(repoFullName, new MultivaluedMapImpl<>(), Response.class,
() -> gh.getPullRequest(jwt.getGhBearerString(installationId), apiVersion, repoFullName, pullRequest));
return prResponse.isPresent() && prResponse.get().getStatus() == 200;
}
/**
* 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;
}
// 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()))));
return out;
}
}
......@@ -136,6 +136,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 +164,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 +182,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 +190,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 +251,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()));
......
......@@ -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) {
......
......@@ -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);
......
......@@ -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');
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment