diff --git a/config/application/secret.properties.sample b/config/application/secret.properties.sample index 4de58a1f7f2ab8df02a2f16c21e3400ec18c9177..99f4d517fdd6d498e991eae6bdd70c0255a4241c 100644 --- a/config/application/secret.properties.sample +++ b/config/application/secret.properties.sample @@ -12,4 +12,8 @@ quarkus.datasource.password= quarkus.datasource.jdbc.url=jdbc:mariadb://mariadb/dev_eclipse_eca %dev.quarkus.datasource.jdbc.url=jdbc:mariadb://${eclipse.internal-host}:10101/dev_eclipse_eca -eclipse.gitlab.access-token= \ No newline at end of file +eclipse.gitlab.access-token= + +## Used to send mail through the EclipseFdn smtp connection +quarkus.mailer.password=YOURGENERATEDAPPLICATIONPASSWORD +quarkus.mailer.username=YOUREMAIL@gmail.com \ No newline at end of file diff --git a/pom.xml b/pom.xml index 93dcd59b58024f1a655009d27ded151729879f6d..ec754369e5983b43b31117963d17f85ad035badf 100644 --- a/pom.xml +++ b/pom.xml @@ -1,8 +1,5 @@ -<?xml version="1.0"?> -<project - xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd" - xmlns="http://maven.apache.org/POM/4.0.0" - xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"> +<?xml version="1.0" encoding="UTF-8"?> +<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <groupId>org.eclipsefoundation</groupId> <artifactId>git-eca</artifactId> @@ -25,8 +22,7 @@ <sonar.tests>src/test</sonar.tests> <sonar.java.coveragePlugin>jacoco</sonar.java.coveragePlugin> <sonar.dynamicAnalysis>reuseReports</sonar.dynamicAnalysis> - <sonar.coverage.jacoco.xmlReportPaths> - ${project.basedir}/target/jacoco-report/jacoco.xml</sonar.coverage.jacoco.xmlReportPaths> + <sonar.coverage.jacoco.xmlReportPaths>${project.basedir}/target/jacoco-report/jacoco.xml</sonar.coverage.jacoco.xmlReportPaths> <sonar.junit.reportPath>${project.build.directory}/surefire-reports</sonar.junit.reportPath> <sonar.host.url>https://sonarcloud.io</sonar.host.url> <sonar.organization>eclipse-foundation-it</sonar.organization> @@ -38,10 +34,8 @@ <id>eclipsefdn</id> <url>https://repo.eclipse.org/content/repositories/eclipsefdn/</url> <releases> - <enabled>true</enabled> </releases> <snapshots> - <enabled>true</enabled> </snapshots> </repository> </repositories> @@ -80,6 +74,10 @@ <groupId>io.quarkus</groupId> <artifactId>quarkus-smallrye-jwt</artifactId> </dependency> + <dependency> + <groupId>io.quarkus</groupId> + <artifactId>quarkus-mailer</artifactId> + </dependency> <!-- Annotation preprocessors - reduce all of the boiler plate --> <dependency> @@ -179,8 +177,7 @@ <configuration> <skipTests>false</skipTests> <systemPropertyVariables> - <java.util.logging.manager> - org.jboss.logmanager.LogManager</java.util.logging.manager> + <java.util.logging.manager>org.jboss.logmanager.LogManager</java.util.logging.manager> <maven.home>${maven.home}</maven.home> </systemPropertyVariables> </configuration> diff --git a/src/main/java/org/eclipsefoundation/git/eca/config/MailerConfig.java b/src/main/java/org/eclipsefoundation/git/eca/config/MailerConfig.java new file mode 100644 index 0000000000000000000000000000000000000000..abf66ffbc95c33208e8caf6fea32da3907ee967b --- /dev/null +++ b/src/main/java/org/eclipsefoundation/git/eca/config/MailerConfig.java @@ -0,0 +1,44 @@ +/** + * Copyright (c) 2025 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: Jordi Gómez <jordi.gomez@eclipse-foundation.org> + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.eclipsefoundation.git.eca.config; + +import java.util.List; +import java.util.Optional; + +import io.smallrye.config.ConfigMapping; + +/** + * Represents configuration for the default mailer service. + * + * @author Martin Lowe + * + */ +@ConfigMapping(prefix = "eclipse.mailer") +public interface MailerConfig { + + public RevalidationAlert revalidationAlert(); + + /** + * This interface defines the contract for specifying recipients and message configuration when sending revalidation alerts within the + * ECA validation process. + */ + public interface RevalidationAlert { + public List<String> to(); + + public MessageConfiguration authorMessage(); + } + + public interface MessageConfiguration { + public Optional<String> replyTo(); + + public Optional<List<String>> bcc(); + } +} diff --git a/src/main/java/org/eclipsefoundation/git/eca/service/MailerService.java b/src/main/java/org/eclipsefoundation/git/eca/service/MailerService.java new file mode 100644 index 0000000000000000000000000000000000000000..1e307f692b13e423c430a3db3679f94c8178b534 --- /dev/null +++ b/src/main/java/org/eclipsefoundation/git/eca/service/MailerService.java @@ -0,0 +1,24 @@ +/** + * Copyright (c) 2025 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/ + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.eclipsefoundation.git.eca.service; + +import org.eclipsefoundation.git.eca.dto.GithubWebhookTracking; + +/** + * Service responsible for sending email notifications. + */ +public interface MailerService { + /** + * Notifies a configured recipient about an stuck revalidation request. + * + * @param tracking the GithubWebhookTracking object containing information about the request + * @param threshold the used threshold for the revalidation alert + */ + void sendRevalidationAlert(GithubWebhookTracking tracking, Integer threshold); +} diff --git a/src/main/java/org/eclipsefoundation/git/eca/service/impl/DefaultMailerService.java b/src/main/java/org/eclipsefoundation/git/eca/service/impl/DefaultMailerService.java new file mode 100644 index 0000000000000000000000000000000000000000..5452c6a088ea6de7e9bf1798aa1fb9aee8090baa --- /dev/null +++ b/src/main/java/org/eclipsefoundation/git/eca/service/impl/DefaultMailerService.java @@ -0,0 +1,71 @@ +/** + * Copyright (c) 2025 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: Jordi Gómez <jordi.gomez@eclipse-foundation.org> + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.eclipsefoundation.git.eca.service.impl; + +import org.eclipsefoundation.git.eca.config.MailerConfig; +import org.eclipsefoundation.git.eca.dto.GithubWebhookTracking; +import org.eclipsefoundation.git.eca.service.MailerService; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import io.quarkus.mailer.Mail; +import io.quarkus.mailer.Mailer; +import io.quarkus.qute.Location; +import io.quarkus.qute.Template; +import jakarta.enterprise.context.ApplicationScoped; + +@ApplicationScoped +public class DefaultMailerService implements MailerService { + public static final Logger LOGGER = LoggerFactory.getLogger(DefaultMailerService.class); + private final MailerConfig config; + private final Mailer mailer; + + @Location("emails/revalidation_alert") + Template revalidationAlertTemplate; + + public DefaultMailerService(MailerConfig config, Mailer mailer) { + this.config = config; + this.mailer = mailer; + } + + @Override + public void sendRevalidationAlert(GithubWebhookTracking tracking, Integer threshold) { + var revalidationAlertConfig = config.revalidationAlert(); + + if (revalidationAlertConfig.to().isEmpty()) { + LOGGER.warn("No recipients configured for revalidation alert. Skipping email notification."); + return; + } + + String subject = "GitHub Webhook Revalidation Alert"; + + Mail messageBuilder = new Mail(); + + messageBuilder.setSubject(subject); + messageBuilder + .setText(revalidationAlertTemplate + .data("attempts", threshold) + .data("requestId", tracking.getId()) + .data("repository", tracking.getRepositoryFullName()) + .data("pullRequest", tracking.getPullRequestNumber()) + .data("lastState", tracking.getLastKnownState()) + .data("lastUpdated", tracking.getLastUpdated().toString()) + .render()); + + messageBuilder.addTo(revalidationAlertConfig.to().toArray(String[]::new)); + revalidationAlertConfig.authorMessage().replyTo().ifPresent(messageBuilder::addReplyTo); + revalidationAlertConfig.authorMessage().bcc().ifPresent(bcc -> messageBuilder.addBcc(bcc.toArray(String[]::new))); + + mailer.send(messageBuilder); + LOGGER.info("Revalidation alert sent to: {}", revalidationAlertConfig.to()); + } + +} diff --git a/src/main/java/org/eclipsefoundation/git/eca/tasks/GithubRevalidationQueue.java b/src/main/java/org/eclipsefoundation/git/eca/tasks/GithubRevalidationQueue.java index bdb9f0c621dd57aefa27e304b5f233401a19adb7..7c638ce091d6e6e7e6fae790227793e9608bdc24 100644 --- a/src/main/java/org/eclipsefoundation/git/eca/tasks/GithubRevalidationQueue.java +++ b/src/main/java/org/eclipsefoundation/git/eca/tasks/GithubRevalidationQueue.java @@ -1,9 +1,8 @@ /** * 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/ + * 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> * @@ -14,11 +13,13 @@ package org.eclipsefoundation.git.eca.tasks; import java.net.URI; import java.util.Arrays; import java.util.List; +import java.util.Objects; import org.eclipse.microprofile.config.inject.ConfigProperty; import org.eclipsefoundation.git.eca.dto.GithubWebhookTracking; import org.eclipsefoundation.git.eca.helper.GithubHelper; import org.eclipsefoundation.git.eca.namespace.GitEcaParameterNames; +import org.eclipsefoundation.git.eca.service.MailerService; import org.eclipsefoundation.http.model.FlatRequestWrapper; import org.eclipsefoundation.http.model.RequestWrapper; import org.eclipsefoundation.http.namespace.DefaultUrlParameterNames; @@ -40,11 +41,10 @@ import jakarta.ws.rs.core.MultivaluedHashMap; import jakarta.ws.rs.core.MultivaluedMap; /** - * Scheduled regular task that will interact with the backend persistence to look for requests that are in a - * failed/unvalidated state after an error while processing that could not be recovered. These requests will be - * reprocessed using the same logic as the standard validation, updating the timestamp on completion and either setting - * the revalidation flag to false or incrementing the number of repeated revalidations needed for the request for - * tracking, depending on the succcess of the revalidation. + * Scheduled regular task that will interact with the backend persistence to look for requests that are in a failed/unvalidated state after + * an error while processing that could not be recovered. These requests will be reprocessed using the same logic as the standard + * validation, updating the timestamp on completion and either setting the revalidation flag to false or incrementing the number of repeated + * revalidations needed for the request for tracking, depending on the succcess of the revalidation. */ @ApplicationScoped public class GithubRevalidationQueue { @@ -56,13 +56,20 @@ public class GithubRevalidationQueue { @ConfigProperty(name = "eclipse.git-eca.tasks.gh-revalidation.enabled", defaultValue = "true") Instance<Boolean> isEnabled; + @ConfigProperty(name = "eclipse.git-eca.tasks.gh-revalidation.notification-enabled", defaultValue = "true") + Instance<Boolean> isRevalidationAlertEnabled; + + @ConfigProperty(name = "eclipse.git-eca.tasks.gh-revalidation.notification-threshold", defaultValue = "100") + Instance<Integer> revalidationThreshold; + @Inject PersistenceDao dao; @Inject FilterService filters; - @Inject GithubHelper validationHelper; + @Inject + MailerService mailerService; @PostConstruct void init() { @@ -71,8 +78,8 @@ public class GithubRevalidationQueue { } /** - * Every 5s, this method will attempt to load a Github webhook validation request that has the needs revalidation flag - * set to true. This will retrieve the oldest request in queue and will attempt to revalidate it. + * Every 5s, this method will attempt to load a Github webhook validation request that has the needs revalidation flag set to true. This + * will retrieve the oldest request in queue and will attempt to revalidate it. */ @Scheduled(every = "${eclipse.git-eca.tasks.gh-revalidation.frequency:60s}") @ActivateRequestContext @@ -101,9 +108,9 @@ public class GithubRevalidationQueue { } /** - * Reprocess the given record, attempting to run the ECA validation logic again. If it passes, the revalidation flag is - * set to false and the time code is updated. If the processing fails again, the failure count gets incremented and the - * updated time is set so that another entry can be updated, as to not block on potentially broken records. + * Reprocess the given record, attempting to run the ECA validation logic again. If it passes, the revalidation flag is set to false and + * the time code is updated. If the processing fails again, the failure count gets incremented and the updated time is set so that + * another entry can be updated, as to not block on potentially broken records. * * @param requestToRevalidate the webhook tracking request to attempt to revalidate * @param wrap the current stubbed request wrapper used for queries. @@ -117,6 +124,10 @@ public class GithubRevalidationQueue { requestToRevalidate .setManualRevalidationCount(requestToRevalidate.getManualRevalidationCount() == null ? 1 : requestToRevalidate.getManualRevalidationCount() + 1); + + // Check if notification threshold is reached and send email if needed + checkRevalidationThreshold(requestToRevalidate); + // wrap in try-catch to avoid errors from stopping the record updates try { // split the full repo name into the org and repo name @@ -126,8 +137,7 @@ public class GithubRevalidationQueue { + "' is in an invalid state (repository full name is not valid)"); } - // run the validation and then check if it succeeded. Use the forced flag since we want to try even if there are no - // changes + // run the validation and then check if it succeeded. Use the forced flag since we want to try even if there are no changes validationHelper .validateIncomingRequest(wrap, repoFullNameParts[0], repoFullNameParts[1], requestToRevalidate.getPullRequestNumber(), true); @@ -151,4 +161,42 @@ public class GithubRevalidationQueue { dao.add(new RDBMSQuery<>(wrap, filters.get(GithubWebhookTracking.class)), Arrays.asList(requestToRevalidate)); } } + + /** + * Checks if the manual revalidation count has reached the configured threshold and sends an email notification if it has and the + * notification alert is enabled. + * + * @param tracking the webhook tracking object + */ + private void checkRevalidationThreshold(GithubWebhookTracking tracking) { + // If revalidation alert is not enabled, do nothing + if (!Boolean.TRUE.equals(isRevalidationAlertEnabled.get())) + return; + + Integer threshold = revalidationThreshold.get(); + + // If manual revalidation count is null or less than threshold, do nothing + if (Objects.isNull(tracking.getManualRevalidationCount()) || tracking.getManualRevalidationCount() < threshold) { + return; + } + + // Check if count equals threshold (to send notification only once when threshold is reached) + if (tracking.getManualRevalidationCount() == threshold) { + sendRevalidationAlert(tracking); + } + } + + /** + * Sends an email alert about a webhook that has reached the revalidation threshold. + * + * @param tracking the webhook tracking object + */ + private void sendRevalidationAlert(GithubWebhookTracking tracking) { + try { + mailerService.sendRevalidationAlert(tracking, revalidationThreshold.get()); + LOGGER.info("Sent revalidation alert email for request ID {}", tracking.getId()); + } catch (RuntimeException e) { + LOGGER.error("Failed to send revalidation alert email for request ID {}", tracking.getId(), e); + } + } } diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index 3f208c281fbbf67b1df19961e2d744619426ea59..c440297168f898549a95c48acef8733a3da18e93 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -65,3 +65,15 @@ eclipse.git-eca.reports.allowed-users=mbarbaro,webdev ## Misc eclipse.system-hook.pool-size=5 + +## Revalidation alert email config +eclipse.git-eca.tasks.gh-revalidation.notification-threshold=100 +eclipse.mailer.revalidation-alert.to=webdev@eclipse-foundation.org + +# Quarkus Mailer +quarkus.mailer.auth-methods=DIGEST-MD5 CRAM-SHA256 CRAM-SHA1 CRAM-MD5 PLAIN LOGIN +quarkus.mailer.from=no-reply@eclipse-foundation.org +# Uses gmail by default, can be overridden +quarkus.mailer.host=smtp.gmail.com +quarkus.mailer.port=465 +quarkus.mailer.ssl=true diff --git a/src/main/resources/templates/emails/revalidation_alert.txt b/src/main/resources/templates/emails/revalidation_alert.txt new file mode 100644 index 0000000000000000000000000000000000000000..bab58a521de8ac6a5950a3f3fdb8d45ea0b2a8e9 --- /dev/null +++ b/src/main/resources/templates/emails/revalidation_alert.txt @@ -0,0 +1,13 @@ +A GitHub webhook has reached the revalidation threshold of {attempts} attempts. + +Details: +- Request ID: {requestId} +- Repository: {repository} +- Pull Request: #{pullRequest} +- Last State: {lastState} +- Last Updated: {lastUpdated} + +Please investigate this issue as it may indicate a persistent problem. + +------------------------------------------------------------------------------------------- +This is an automated email from api.eclipse.org/git/eca. Please do not reply to this email. \ No newline at end of file diff --git a/src/test/resources/application.properties b/src/test/resources/application.properties index 44c99d18e3ef480fc4adacf55efaa8b74cc7bd56..bd6e17d86aaa0dd58b868dcb16386a4bdc8f1ef4 100644 --- a/src/test/resources/application.properties +++ b/src/test/resources/application.properties @@ -23,6 +23,9 @@ quarkus.keycloak.devservices.enabled=false quarkus.oidc-client.enabled=false smallrye.jwt.sign.key.location=test.pem +# Quarkus Mailer +quarkus.mailer.mock=true + # hCaptcha test key and secret eclipse.hcaptcha.site-key=20000000-ffff-ffff-ffff-000000000002 eclipse.hcaptcha.secret=0x0000000000000000000000000000000000000000 @@ -40,4 +43,4 @@ eclipse.git-eca.tasks.gh-installation.enabled=false eclipse.gitlab.access-token=token_val ## Disable private project scan in test mode -eclipse.scheduled.private-project.enabled=false \ No newline at end of file +eclipse.scheduled.private-project.enabled=false