Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
95253ea
Notifications
krystian-panek-vmltech Aug 29, 2025
873cb39
Notifications
krystian-panek-vmltech Aug 29, 2025
ffa1fc5
Notifications
krystian-panek-vmltech Aug 29, 2025
3e20df4
Notifications
krystian-panek-vmltech Aug 29, 2025
7963d04
Notifications
krystian-panek-vmltech Aug 29, 2025
fa8b0a2
Safer Slack builder
krystian-panek-vmltech Sep 1, 2025
9d2de8c
Teams payload and client
krystian-panek-vmltech Sep 1, 2025
eec82e2
Extension + notifications
krystian-panek-vmltech Sep 1, 2025
0dc5524
refactor: extract notification logic from AcmeFacade into standalone …
krystian-panek-vmltech Sep 1, 2025
93f5ded
Align to AEMC
krystian-panek-vmltech Sep 3, 2025
f9c4906
Notifier optionality
krystian-panek-vmltech Sep 3, 2025
10d49c3
Misc GUI
krystian-panek-vmltech Sep 3, 2025
6096d83
Send message method
krystian-panek-vmltech Sep 3, 2025
a10200f
Multi-notifier
krystian-panek-vmltech Sep 3, 2025
a378e1c
Multi-notifier
krystian-panek-vmltech Sep 3, 2025
0e7254d
Multi-notifier
krystian-panek-vmltech Sep 3, 2025
c65a15a
Instance name
krystian-panek-vmltech Sep 3, 2025
233389e
Notifications for Slack hardened
krystian-panek-vmltech Sep 3, 2025
35df26c
Stricter notifiers
krystian-panek-vmltech Sep 3, 2025
1a3a4c6
Executor with notifications
krystian-panek-vmltech Sep 3, 2025
8d5e437
Executor with notifications
krystian-panek-vmltech Sep 3, 2025
85e1afb
Refactoring
krystian-panek-vmltech Sep 3, 2025
0e7ecb8
Bug fixes
krystian-panek-vmltech Sep 4, 2025
d96c23a
Bug fixes
krystian-panek-vmltech Sep 4, 2025
821be51
Bug fixes
krystian-panek-vmltech Sep 4, 2025
6cb416e
Bug fixes
krystian-panek-vmltech Sep 4, 2025
3802892
Output and error in notifications
krystian-panek-vmltech Sep 4, 2025
3826fcc
Hardening
krystian-panek-vmltech Sep 4, 2025
ebbdf53
Hardening
krystian-panek-vmltech Sep 4, 2025
4acf40d
Hardening
krystian-panek-vmltech Sep 4, 2025
19765cd
Hardening
krystian-panek-vmltech Sep 4, 2025
e040fe2
Hardening
krystian-panek-vmltech Sep 4, 2025
d9cc330
Hardening
krystian-panek-vmltech Sep 4, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .vscode/launch.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
"name": "Attach to Remote Program",
"request": "attach",
"hostName": "localhost",
"port": "14502",
"port": "15502",
"projectName": "acm.core"
}
]
Expand Down
2 changes: 0 additions & 2 deletions Taskfile.yml
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,6 @@ env:
AEM_ENV: '{{.AEM_ENV | default "local"}}'
AEM_INSTANCE_PROCESSING_MODE: auto
AEM_OUTPUT_VALUE: NONE
JAVA_HOME:
sh: sh aemw vendor list --output-value javaHome

dotenv:
- '.env' # VCS-ignored, user-specific
Expand Down
5 changes: 4 additions & 1 deletion aem/default/etc/aem.yml
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,10 @@ instance:
skip: false
pids:
include: ['dev.vml.es.acm.core.*', 'com.day.crx.packaging.*', 'org.apache.sling.installer.*']
exclude: ['org.apache.sling.installer.hc.*', 'org.apache.sling.installer.core.impl.console.*']
exclude:
- 'org.apache.sling.installer.hc.*'
- 'org.apache.sling.installer.core.impl.console.*'
- 'dev.vml.es.acm.core.notification.*.*Factory'
match:
"disabled": []
"no config": []
Expand Down
5 changes: 5 additions & 0 deletions core/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -235,6 +235,11 @@ DynamicImport-Package: *
<groupId>org.apache.commons</groupId>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.apache.httpcomponents</groupId>
<artifactId>httpclient</artifactId>
<version>4.5.4</version>
</dependency>
<dependency>
<artifactId>org.apache.jackrabbit.vault</artifactId>
<version>3.2.8</version>
Expand Down
2 changes: 2 additions & 0 deletions core/src/main/java/dev/vml/es/acm/core/AcmConstants.java
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

public class AcmConstants {

public static final String NAME = "AEM Content Manager";

public static final String CODE = "acm";

public static final String SETTINGS_ROOT = "/conf/acm/settings";
Expand Down
57 changes: 52 additions & 5 deletions core/src/main/java/dev/vml/es/acm/core/code/CodeContext.java
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import dev.vml.es.acm.core.code.script.ExtensionScript;
import dev.vml.es.acm.core.format.Formatter;
import dev.vml.es.acm.core.mock.MockContext;
import dev.vml.es.acm.core.notification.NotificationManager;
import dev.vml.es.acm.core.osgi.OsgiContext;
import dev.vml.es.acm.core.replication.Activator;
import dev.vml.es.acm.core.repo.Repo;
Expand All @@ -14,6 +15,7 @@
import java.util.Map;
import java.util.stream.Collectors;
import org.apache.sling.api.resource.ResourceResolver;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class CodeContext {
Expand All @@ -28,11 +30,31 @@ public class CodeContext {

private final Locker locker;

private final Logger log;

private final Repo repo;

private final Acl acl;

private final Activator activator;

private final Formatter formatter;

private final NotificationManager notifier;

public CodeContext(OsgiContext osgiContext, ResourceResolver resourceResolver) {
this.osgiContext = osgiContext;
this.resourceResolver = resourceResolver;

this.log = LoggerFactory.getLogger(getClass());
this.locker = new Locker(resourceResolver);
this.repo = new Repo(resourceResolver);
this.acl = new Acl(resourceResolver);
this.activator = new Activator(resourceResolver, osgiContext);
this.formatter = new Formatter();
this.notifier = osgiContext.getService(NotificationManager.class);
this.binding = createBinding();

this.extensionScripts = findExtensionScripts();
}

Expand All @@ -46,14 +68,15 @@ private List<ExtensionScript> findExtensionScripts() {
public Binding createBinding() {
Binding result = new Binding();

result.setVariable("log", LoggerFactory.getLogger(getClass()));
result.setVariable("log", log);
result.setVariable("resourceResolver", resourceResolver);
result.setVariable("locker", locker);
result.setVariable("osgi", osgiContext);
result.setVariable("repo", new Repo(resourceResolver));
result.setVariable("acl", new Acl(resourceResolver));
result.setVariable("formatter", new Formatter());
result.setVariable("activator", new Activator(resourceResolver, osgiContext.getReplicator()));
result.setVariable("repo", repo);
result.setVariable("acl", acl);
result.setVariable("formatter", formatter);
result.setVariable("activator", activator);
result.setVariable("notifier", notifier);

return result;
}
Expand Down Expand Up @@ -99,4 +122,28 @@ public OsgiContext getOsgiContext() {
public Locker getLocker() {
return locker;
}

public Repo getRepo() {
return repo;
}

public Acl getAcl() {
return acl;
}

public Logger getLog() {
return log;
}

public Activator getActivator() {
return activator;
}

public Formatter getFormatter() {
return formatter;
}

public NotificationManager getNotifier() {
return notifier;
}
}
22 changes: 11 additions & 11 deletions core/src/main/java/dev/vml/es/acm/core/code/Conditions.java
Original file line number Diff line number Diff line change
Expand Up @@ -175,35 +175,35 @@ public boolean instanceChanged(Execution execution) {
}

public boolean isInstanceRunMode(String runMode) {
return getInstanceInfo().isRunMode(runMode);
}

private InstanceInfo getInstanceInfo() {
return executionContext.getCodeContext().getOsgiContext().getInstanceInfo();
return instanceInfo().isRunMode(runMode);
}

public boolean isInstanceAuthor() {
return getInstanceInfo().isAuthor();
return instanceInfo().isAuthor();
}

public boolean isInstancePublish() {
return getInstanceInfo().isPublish();
return instanceInfo().isPublish();
}

public boolean isInstanceOnPrem() {
return getInstanceInfo().getType() == InstanceType.ON_PREM;
return instanceInfo().getType() == InstanceType.ON_PREM;
}

public boolean isInstanceCloud() {
return getInstanceInfo().getType().isCloud();
return instanceInfo().getType().isCloud();
}

public boolean isInstanceCloudContainer() {
return getInstanceInfo().getType() == InstanceType.CLOUD_CONTAINER;
return instanceInfo().getType() == InstanceType.CLOUD_CONTAINER;
}

public boolean isInstanceCloudSdk() {
return getInstanceInfo().getType() == InstanceType.CLOUD_SDK;
return instanceInfo().getType() == InstanceType.CLOUD_SDK;
}

public InstanceInfo instanceInfo() {
return executionContext.getCodeContext().getOsgiContext().getInstanceInfo();
}

// Executable-based
Expand Down
98 changes: 93 additions & 5 deletions core/src/main/java/dev/vml/es/acm/core/code/Executor.java
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,20 @@
import dev.vml.es.acm.core.AcmConstants;
import dev.vml.es.acm.core.AcmException;
import dev.vml.es.acm.core.code.script.ContentScript;
import dev.vml.es.acm.core.format.TemplateFormatter;
import dev.vml.es.acm.core.instance.InstanceSettings;
import dev.vml.es.acm.core.notification.NotificationManager;
import dev.vml.es.acm.core.osgi.InstanceInfo;
import dev.vml.es.acm.core.osgi.OsgiContext;
import dev.vml.es.acm.core.util.ResolverUtils;
import java.text.SimpleDateFormat;
import java.util.Arrays;
import java.util.Date;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Optional;
import java.util.concurrent.ConcurrentHashMap;
import java.util.regex.Pattern;
import org.apache.commons.lang3.StringUtils;
import org.apache.sling.api.resource.LoginException;
import org.apache.sling.api.resource.ResourceResolver;
Expand Down Expand Up @@ -45,6 +54,31 @@ public class Executor {
name = "Log Printing Names",
description = "Additional loggers to print logs from (class names or package names)")
String[] logPrintingNames() default {CodeLoggerPrinter.NAME_ACL, CodeLoggerPrinter.NAME_REPO};

@AttributeDefinition(
name = "Notification Enabled",
description = "Enables notifications for completed executions.")
boolean notificationEnabled() default true;

@AttributeDefinition(
name = "Notification Executable IDs",
description = "Allow to control with regular expressions which executables should be notified about.")
String[] notificationExecutableIds() default {"/conf/acm/settings/script/automatic/.*"};

@AttributeDefinition(
name = "Notification Title",
description = "Template variables: context, execution, statusIcon, statusHere")
String notificationTitle() default "${statusIcon} ACM Code Execution";

@AttributeDefinition(
name = "Notification Text",
description = "Template variables: context, execution, statusIcon, statusHere")
String notificationText() default "Completed: ${execution.executable.id} ${statusHere}";

@AttributeDefinition(
name = "Notification Details Length",
description = "Max length of the output and error. Use negative value to skip abbreviation.")
int notificationDetailsLength() default 256;
}

@Reference
Expand All @@ -53,6 +87,9 @@ public class Executor {
@Reference
private OsgiContext osgiContext;

@Reference
private NotificationManager notifier;

private Config config;

private final Map<String, ExecutionStatus> statuses = new ConcurrentHashMap<>();
Expand Down Expand Up @@ -88,11 +125,8 @@ public Execution execute(ExecutionContext context) throws AcmException {
context.getCodeContext().prepareRun(context);
ImmediateExecution execution = executeImmediately(context);
if (context.getMode() == ExecutionMode.RUN) {
if (context.isHistory() && (context.isDebug() || (execution.getStatus() != ExecutionStatus.SKIPPED))) {
ExecutionHistory history =
new ExecutionHistory(context.getCodeContext().getResourceResolver());
history.save(context, execution);
}
handleHistory(context, execution);
handleNotifications(context, execution);
context.getCodeContext().completeRun(execution);
}
return execution;
Expand Down Expand Up @@ -162,6 +196,60 @@ public Optional<ExecutionStatus> checkStatus(String executionId) {
return Optional.ofNullable(statuses.get(executionId));
}

private void handleHistory(ExecutionContext context, ImmediateExecution execution) {
if (context.isHistory() && (context.isDebug() || (execution.getStatus() != ExecutionStatus.SKIPPED))) {
ExecutionHistory history =
new ExecutionHistory(context.getCodeContext().getResourceResolver());
history.save(context, execution);
}
}

private void handleNotifications(ExecutionContext context, ImmediateExecution execution) {
String executableId = execution.getExecutable().getId();
if (!config.notificationEnabled()
|| !notifier.isConfigured()
|| Arrays.stream(config.notificationExecutableIds())
.noneMatch(regex -> Pattern.matches(regex, executableId))) {
return;
}

Map<String, Object> templateVars = new LinkedHashMap<>();
templateVars.put("context", context);
templateVars.put("execution", execution);
templateVars.put(
"statusIcon",
execution.getStatus() == ExecutionStatus.SUCCEEDED
? "✅"
: (execution.getStatus() == ExecutionStatus.FAILED ? "❌" : "⚠️"));
templateVars.put("statusHere", execution.getStatus() == ExecutionStatus.SUCCEEDED ? "" : "@here");
TemplateFormatter templateFormatter =
context.getCodeContext().getFormatter().getTemplate();
String title = StringUtils.trim(templateFormatter.renderString(config.notificationTitle(), templateVars));
String text = StringUtils.trim(templateFormatter.renderString(config.notificationText(), templateVars));

Map<String, Object> fields = new LinkedHashMap<>();
fields.put("Status", execution.getStatus().name().toLowerCase());
fields.put("Time", new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date()));
fields.put("Duration", execution.getDuration() + "ms");

InstanceInfo instanceInfo = context.getCodeContext().getOsgiContext().getInstanceInfo();
InstanceSettings instanceSettings = new InstanceSettings(instanceInfo);
String instanceRoleName = instanceSettings.getRole().name().toLowerCase();
String instanceId = instanceSettings.getId();
String instanceDesc = instanceId.toLowerCase().contains(instanceRoleName)
? instanceId
: instanceId + " (" + instanceRoleName + ")";
fields.put("Instance", instanceDesc);

int detailsMaxLength = config.notificationDetailsLength();
String output = StringUtils.defaultIfBlank(execution.getOutput(), "(empty)");
String error = StringUtils.defaultIfBlank(execution.getError(), "(empty)");
fields.put("Output", detailsMaxLength < 0 ? output : StringUtils.abbreviate(output, detailsMaxLength));
fields.put("Error", detailsMaxLength < 0 ? error : StringUtils.abbreviate(error, detailsMaxLength));

notifier.sendMessage(title, text, fields);
}

public Description describe(ExecutionContext context) {
ImmediateExecution.Builder execution = new ImmediateExecution.Builder(context).start();
try {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@
import dev.vml.es.acm.core.osgi.InstanceType;
import dev.vml.es.acm.core.util.DateUtils;
import java.io.Serializable;
import java.lang.management.ManagementFactory;
import java.util.Set;

public class InstanceSettings implements Serializable {

Expand All @@ -16,11 +18,14 @@ public class InstanceSettings implements Serializable {

private final InstanceType type;

private final Set<String> runModes;

public InstanceSettings(InstanceInfo instanceInfo) {
this.id = "default"; // TODO - replace with Sling ID
this.id = ManagementFactory.getRuntimeMXBean().getName();
this.timezoneId = DateUtils.TIMEZONE_ID;
this.role = instanceInfo.getRole();
this.type = instanceInfo.getType();
this.runModes = instanceInfo.getRunModes();
}

public String getId() {
Expand All @@ -38,4 +43,8 @@ public InstanceRole getRole() {
public InstanceType getType() {
return type;
}

public Set<String> getRunModes() {
return runModes;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package dev.vml.es.acm.core.notification;

import dev.vml.es.acm.core.AcmException;

public class NotificationException extends AcmException {

public NotificationException(String message) {
super(message);
}

public NotificationException(String message, Throwable cause) {
super(message, cause);
}
}
Loading