Compare commits
27
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
da193099b6 | ||
|
|
d1215ad478
|
||
|
|
755a1e2465 | ||
|
|
0c3c1564c3
|
||
|
|
94bed610cd | ||
|
|
5d863aed12 | ||
|
|
68137d6fff | ||
|
|
ba033f90bc
|
||
|
|
33b70be266 | ||
|
|
d11e38139c
|
||
|
|
a9415f93d9 | ||
|
|
50f31a9040
|
||
|
|
1051f1780d | ||
|
|
03c813583c
|
||
|
|
640e37f7c2 | ||
|
|
5bfd7f21e4
|
||
|
|
5a77659e85 | ||
|
|
e1c0d1f7df
|
||
|
|
f6b26737ae | ||
|
|
2e516c6f0f
|
||
|
|
5f40c93727 | ||
|
|
12a17808a8
|
||
|
|
f495dcc9ae | ||
|
|
040fb3e35c
|
||
|
|
a087e8b1f3 | ||
|
|
24c936453f
|
||
|
|
1ff27e90be |
No files matched your search
@@ -21,5 +21,6 @@ jobs:
|
||||
release-rc:
|
||||
if: github.event_name == 'workflow_dispatch' || (github.event.pull_request.merged == true && contains(github.event.pull_request.labels.*.name, 'release:rc'))
|
||||
uses: QRun-IO/.github/.github/workflows/qbit-release-rc.yml@main
|
||||
secrets: inherit
|
||||
with:
|
||||
dry_run: ${{ github.event_name == 'workflow_dispatch' && inputs.dry_run }}
|
||||
@@ -17,6 +17,8 @@ The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/) and
|
||||
- `EmailTablesGuardrailTest`: every table is used by runtime code, seeded or listed as starting empty with a reason, and has help.
|
||||
|
||||
### Fixed
|
||||
- The `email.readiness` health message now names each missing item in plain words and where to set it (for example "Brand postal address is not set (Email > Brand > open the default brand > Postal address)") and says transactional email is unaffected by marketing-only gaps. The `failedChecks` detail is unchanged; a new `howToFix` detail lists the same text.
|
||||
- Derive the qbit metadata version from the Maven artifact version so released artifacts report their release version.
|
||||
- `email_template.ownerQbit` was stored but never read, and the built-in template did not set it. The built-in now names `qbit-email`, the field is read-only with help, and messages sent from a template name its owner qbit as `senderQbit` unless the request names one.
|
||||
|
||||
### Added
|
||||
|
||||
@@ -22,7 +22,7 @@
|
||||
<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">
|
||||
|
||||
<properties>
|
||||
<revision>0.1.0-SNAPSHOT</revision>
|
||||
<revision>0.1.0-RC.9</revision>
|
||||
<maven.compiler.source>21</maven.compiler.source>
|
||||
<maven.compiler.target>21</maven.compiler.target>
|
||||
<jacoco.version>0.8.15</jacoco.version>
|
||||
@@ -175,6 +175,7 @@
|
||||
<filtering>true</filtering>
|
||||
<includes>
|
||||
<include>help/*.yaml</include>
|
||||
<include>qbit-email.properties</include>
|
||||
</includes>
|
||||
</resource>
|
||||
<resource>
|
||||
@@ -182,10 +183,20 @@
|
||||
<filtering>false</filtering>
|
||||
<excludes>
|
||||
<exclude>help/*.yaml</exclude>
|
||||
<exclude>qbit-email.properties</exclude>
|
||||
</excludes>
|
||||
</resource>
|
||||
</resources>
|
||||
<plugins>
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-surefire-plugin</artifactId>
|
||||
<configuration>
|
||||
<systemPropertyVariables combine.children="append">
|
||||
<qbit.artifactVersion>${project.version}</qbit.artifactVersion>
|
||||
</systemPropertyVariables>
|
||||
</configuration>
|
||||
</plugin>
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-checkstyle-plugin</artifactId>
|
||||
|
||||
@@ -21,10 +21,13 @@
|
||||
package com.kingsrook.qbits.email;
|
||||
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.util.ArrayList;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Properties;
|
||||
import com.kingsrook.qbits.email.app.EmailApp;
|
||||
import com.kingsrook.qbits.email.bridge.EmailOutboxMessagingProviderMetaData;
|
||||
import com.kingsrook.qbits.email.delivery.EmailAlerts;
|
||||
@@ -53,13 +56,41 @@ public class EmailQBitProducer implements QBitMetaDataProducer<EmailQBitConfig>
|
||||
{
|
||||
public static final String GROUP_ID = "com.kingsrook.qbits";
|
||||
public static final String ARTIFACT_ID = "qbit-email";
|
||||
public static final String VERSION = "0.1.0";
|
||||
public static final String VERSION = loadVersion();
|
||||
|
||||
private EmailQBitConfig qBitConfig;
|
||||
private String namespace;
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Read the artifact version from the Maven-filtered resource.
|
||||
*******************************************************************************/
|
||||
private static String loadVersion()
|
||||
{
|
||||
try(InputStream inputStream = EmailQBitProducer.class.getClassLoader().getResourceAsStream("qbit-email.properties"))
|
||||
{
|
||||
if(inputStream == null)
|
||||
{
|
||||
throw new IllegalStateException("Missing qbit-email.properties; build with Maven to generate the artifact version");
|
||||
}
|
||||
Properties properties = new Properties();
|
||||
properties.load(inputStream);
|
||||
String version = properties.getProperty("version");
|
||||
if(version == null || version.isBlank() || version.contains("${"))
|
||||
{
|
||||
throw new IllegalStateException("Missing or unfiltered version in qbit-email.properties; build with Maven");
|
||||
}
|
||||
return (version);
|
||||
}
|
||||
catch(IOException e)
|
||||
{
|
||||
throw new IllegalStateException("Could not read artifact version from qbit-email.properties", e);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Fluent setter for the config.
|
||||
*******************************************************************************/
|
||||
|
||||
@@ -27,6 +27,7 @@ import java.util.EnumSet;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import java.util.stream.Collectors;
|
||||
import com.kingsrook.qbits.email.EmailRuntime;
|
||||
import com.kingsrook.qbits.email.api.EmailApi;
|
||||
import com.kingsrook.qbits.email.api.EmailApiCategory;
|
||||
import com.kingsrook.qbits.email.api.EmailApiReadinessCheck;
|
||||
@@ -102,7 +103,7 @@ public class EmailHealthChecks implements HealthCheckSupplier
|
||||
{
|
||||
return (new HealthCheckMetaData().withName(name).withLabel(name).withComponent("email")
|
||||
.withCategory(HealthCategory.EMAIL).withKind(HealthCheckKind.DEPENDENCY)
|
||||
.withIntervalSeconds(300).withTimeoutMs(5000).withCheckInstance(input ->
|
||||
.withIntervalSeconds(EmailHealthWidget.CHECK_INTERVAL_MINUTES * 60).withTimeoutMs(5000).withCheckInstance(input ->
|
||||
{
|
||||
try
|
||||
{
|
||||
@@ -134,8 +135,38 @@ public class EmailHealthChecks implements HealthCheckSupplier
|
||||
ready &= readiness.ready();
|
||||
failed.addAll(readiness.failedChecks());
|
||||
}
|
||||
return ((ready ? HealthCheckOutcome.up("Email configuration ready") : HealthCheckOutcome.degraded("Email configuration incomplete"))
|
||||
.withDetails(new HealthDetails().withValue("failedChecks", failed.stream().map(Enum::name).collect(Collectors.joining(",")))));
|
||||
if(ready)
|
||||
{
|
||||
return (HealthCheckOutcome.up("Email configuration ready").withDetails(new HealthDetails().withValue("failedChecks", "")));
|
||||
}
|
||||
var config = EmailRuntime.config(input.getQInstance());
|
||||
String app = config == null || config.getAppLabel() == null ? "Email" : config.getAppLabel();
|
||||
List<String> fixes = failed.stream().map(check -> fix(check, app)).toList();
|
||||
String message = "Email configuration incomplete: " + String.join(" ", fixes);
|
||||
String details = String.join("\n", fixes);
|
||||
return (HealthCheckOutcome.degraded(message.length() > 500 ? message.substring(0, 500) : message)
|
||||
.withDetails(new HealthDetails().withValue("failedChecks", failed.stream().map(Enum::name).collect(Collectors.joining(",")))
|
||||
.withValue("howToFix", details.length() > 4000 ? details.substring(0, 4000) : details)));
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Plain-words statement of one missing prerequisite and where to set it.
|
||||
*******************************************************************************/
|
||||
private static String fix(EmailApiReadinessCheck check, String app)
|
||||
{
|
||||
return (switch(check)
|
||||
{
|
||||
case NOT_INSTALLED -> "The Email qbit is not installed.";
|
||||
case PROVIDER_MISSING -> "No enabled default email provider (" + app + " > Provider: add one and tick Enabled and Is Default).";
|
||||
case PROVIDER_INVALID -> "The default email provider settings are invalid (" + app + " > Provider: open the default provider and correct it).";
|
||||
case PHI_PROVIDER -> "PHI email needs a PHI-enabled provider with open and click tracking off (" + app + " > Provider).";
|
||||
case PUBLIC_BASE_URL -> "Public base URL is not set (the publicBaseUrl setting in the Email qbit config). Needed for marketing unsubscribe links; transactional email is unaffected.";
|
||||
case UNSUBSCRIBE_KEY -> "The QQQ_EMAIL_UNSUBSCRIBE_KEY secret is not set (deployment environment variable). Needed for marketing unsubscribe links; transactional email is unaffected.";
|
||||
case BRAND_POSTAL_ADDRESS -> "Brand postal address is not set (" + app + " > Brand > open the default brand > Postal address). Needed for marketing email footers; transactional email is unaffected.";
|
||||
case BODY_KEY -> "The QQQ_EMAIL_BODY_KEY secret is not set (deployment environment variable). Needed for PHI email.";
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -47,6 +47,31 @@ import com.kingsrook.qqq.backend.core.utils.JsonUtils;
|
||||
*******************************************************************************/
|
||||
public class EmailHealthWidget extends AbstractWidgetRenderer
|
||||
{
|
||||
public static final String NOT_CHECKED = "Not checked yet";
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////////
|
||||
// the interval of the email health checks (EmailHealthChecks), in minutes //
|
||||
//////////////////////////////////////////////////////////////////////////////
|
||||
static final int CHECK_INTERVAL_MINUTES = 5;
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Human words for a health status; unknown means no check has reported yet.
|
||||
*******************************************************************************/
|
||||
static String wording(String status)
|
||||
{
|
||||
return (switch(status)
|
||||
{
|
||||
case "UP" -> "Healthy";
|
||||
case "DEGRADED" -> "Degraded";
|
||||
case "DOWN" -> "Down";
|
||||
default -> NOT_CHECKED;
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Read the persisted rollup as the viewer, preserving health permissions.
|
||||
*******************************************************************************/
|
||||
@@ -72,8 +97,8 @@ public class EmailHealthWidget extends AbstractWidgetRenderer
|
||||
}
|
||||
}
|
||||
}
|
||||
StatisticsData data = new StatisticsData().withCount(status).withCountFontSize("1rem")
|
||||
.withCountContext(!installed ? "Email - health monitoring not configured" : readable ? "Email - System Health" : "Email - health access required");
|
||||
String context = !installed ? "Email - health monitoring not configured" : readable ? "Email - System Health. Checks run every " + CHECK_INTERVAL_MINUTES + " minutes" : "Email - health access required";
|
||||
StatisticsData data = new StatisticsData().withCount(wording(status)).withCountFontSize("1rem").withCountContext(context);
|
||||
if(readable)
|
||||
{
|
||||
data.withCountURL("/app/health_check?filter=" + URLEncoder.encode(JsonUtils.toJson(EmailStore.equal("component", "email")), StandardCharsets.UTF_8));
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
version=${project.version}
|
||||
@@ -48,13 +48,17 @@ class EmailQBitProducerTest
|
||||
assertThat(producer.isEnabled()).isTrue();
|
||||
assertThat(producer.getNamespace()).isNull();
|
||||
assertThat(producer.getQBitConfig().getBackendName()).isEqualTo("memory");
|
||||
String artifactVersion = System.getProperty("qbit.artifactVersion");
|
||||
assertThat(artifactVersion).isNotBlank();
|
||||
assertThat(EmailQBitProducer.VERSION).isEqualTo(artifactVersion);
|
||||
assertThat(producer.getQBitMetaData().getVersion()).isEqualTo(artifactVersion);
|
||||
assertThat(producer.produce(instance)).isNotNull();
|
||||
assertThat(instance.getQBits()).isNotEmpty();
|
||||
assertThat(instance.getTable("helpContent")).isNull();
|
||||
assertThat(HelpBundles.of(instance).getBundles()).singleElement().satisfies(bundle ->
|
||||
{
|
||||
assertThat(bundle.getId()).isEqualTo("qbit-email");
|
||||
assertThat(bundle.getVersion()).matches("\\d+\\.\\d+\\.\\d+(-SNAPSHOT|-RC\\.\\d+)?");
|
||||
assertThat(bundle.getVersion()).isEqualTo(artifactVersion).isEqualTo(EmailQBitProducer.VERSION);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -165,6 +165,8 @@ class EmailHealthChecksTest
|
||||
assertThat(outcome.getStatus()).isEqualTo(HealthStatus.DEGRADED);
|
||||
assertThat(outcome.getDetails().getValues().get("failedChecks").toString().split(","))
|
||||
.containsExactly("PUBLIC_BASE_URL", "BRAND_POSTAL_ADDRESS");
|
||||
assertThat(outcome.getMessage()).contains("Brand postal address is not set").contains("Brand > open the default brand > Postal address").contains("transactional email is unaffected").hasSizeLessThanOrEqualTo(500);
|
||||
assertThat(outcome.getDetails().getValues().get("howToFix").toString()).contains("Public base URL is not set").contains("Brand postal address is not set");
|
||||
configureMarketing();
|
||||
assertThat(run(readinessCheck()).getStatus()).isEqualTo(HealthStatus.UP);
|
||||
update("enabled", false);
|
||||
|
||||
@@ -62,7 +62,7 @@ class EmailHealthWidgetTest
|
||||
var instance = H2Support.setup(config -> config.withSecretResolver(name -> null));
|
||||
assertThat(instance.getWidget("emailHealth").getType()).isEqualTo(WidgetType.STATISTICS.getType());
|
||||
var data = render();
|
||||
assertThat(data.getCount()).isEqualTo("UNKNOWN");
|
||||
assertThat(data.getCount()).isEqualTo("Not checked yet");
|
||||
assertThat(data.getCountContext()).contains("not configured");
|
||||
assertThat(data.getCountURL()).isNull();
|
||||
}
|
||||
@@ -88,15 +88,18 @@ class EmailHealthWidgetTest
|
||||
assertThat(render().getCountURL()).isNull();
|
||||
QContext.getQSession().setPermissions(Set.of("email.view", "health.read"));
|
||||
instance.setEnforcePermissionsInCoreActions(true);
|
||||
assertThat(render().getCount()).isEqualTo("UNKNOWN");
|
||||
assertThat(render().getCount()).isEqualTo("Not checked yet");
|
||||
assertThat(render().getCountContext()).contains("Checks run every 5 minutes");
|
||||
connection.createStatement().execute("INSERT INTO health_component VALUES (1, 'email', 'DOWN'), (2, 'other', 'UP')");
|
||||
assertThat(render().getCount()).isEqualTo("DOWN");
|
||||
assertThat(render().getCount()).isEqualTo("Down");
|
||||
String link = URLDecoder.decode(render().getCountURL(), StandardCharsets.UTF_8);
|
||||
assertThat(link).startsWith("/app/health_check?filter=").contains("component", "email");
|
||||
connection.createStatement().execute("UPDATE health_component SET status = 'UP' WHERE id = 1");
|
||||
assertThat(render().getCount()).isEqualTo("UP");
|
||||
assertThat(render().getCount()).isEqualTo("Healthy");
|
||||
connection.createStatement().execute("UPDATE health_component SET status = 'DEGRADED' WHERE id = 1");
|
||||
assertThat(render().getCount()).isEqualTo("Degraded");
|
||||
connection.createStatement().execute("UPDATE health_component SET status = 'S3NT1NEL-secret' WHERE id = 1");
|
||||
assertThat(render().getCount()).isEqualTo("UNKNOWN");
|
||||
assertThat(render().getCount()).isEqualTo("Not checked yet");
|
||||
assertThat(render().getCountContext()).doesNotContain("S3NT1NEL-secret");
|
||||
}
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user