Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
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
Original file line number Diff line number Diff line change
Expand Up @@ -516,7 +516,14 @@ private static Path nodeSourcePath(Path folder, Map<String, Object> node) {
return result;
}

static String safeNodeFileName(Map<String, Object> node) {
/**
* Returns the stable file-system-safe base name used to persist a developer
* Python node's source.
*
* @param node validated Automation node
* @return stable source file base name without the {@code .py} suffix
*/
public static String safeNodeFileName(Map<String, Object> node) {
String nodeId = (String) node.get(AutomationConstants.NODE_FIELD_ID);
if (nodeId == null || nodeId.isBlank()) {
throw new IllegalArgumentException("Automation node id must be nonblank for source persistence.");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@
* semantics. At a branch merge, outputs produced on only one branch are marked
* conditional and use a safe {@code scope.get(...)} insertion expression.
*/
final class AutomationScopeCatalog {
public final class AutomationScopeCatalog {

private AutomationScopeCatalog() {
}
Expand All @@ -59,7 +59,7 @@ private AutomationScopeCatalog() {
* @param definition validated Automation graph
* @return ordered variables available or conditionally available at each node
*/
static Map<String, List<Map<String, Object>>> variablesByNode(
public static Map<String, List<Map<String, Object>>> variablesByNode(
AutomationDefinitionValidator.ValidatedDefinition definition) {
List<Map<String, Object>> orderedNodes = AutomationRuntime.nodesForRun(definition);
Map<String, List<String>> predecessors = predecessors(definition);
Expand Down
120 changes: 111 additions & 9 deletions src/prerna/reactor/automation/project/AutomationProjectService.java
Original file line number Diff line number Diff line change
Expand Up @@ -27,18 +27,29 @@
*******************************************************************************/
package prerna.reactor.automation.project;

import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.locks.ReentrantLock;
import java.util.function.Function;

import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.eclipse.jgit.api.AddCommand;
import org.eclipse.jgit.api.Git;
import org.eclipse.jgit.api.Status;
import org.eclipse.jgit.api.errors.GitAPIException;
import org.eclipse.jgit.errors.NoWorkTreeException;
import org.javatuples.Pair;

import prerna.auth.AccessToken;
import prerna.auth.User;
import prerna.auth.utils.SecurityEngineUtils;
import prerna.auth.utils.SecurityProjectUtils;
Expand All @@ -54,7 +65,6 @@
import prerna.util.AssetUtility;
import prerna.util.ProjectSyncUtility;
import prerna.util.Utility;
import prerna.util.git.GitRepoUtils;

/**
* Coordinates project-level access, persistence, and derived assets for
Expand All @@ -73,6 +83,7 @@
public final class AutomationProjectService {

private static final Logger classLogger = LogManager.getLogger(AutomationProjectService.class);
private static final String UPDATE_COMMIT_MESSAGE = "Update automation definition";

private AutomationProjectService() {
}
Expand Down Expand Up @@ -226,6 +237,28 @@ public static <T> T withLockedDefinition(String projectId,
*/
public static AutomationDefinitionService.DefinitionFiles saveDefinition(String projectId, String definitionJson,
Map<String, String> nodeSources, String expectedRevision, User user) {
return saveDefinition(projectId, definitionJson, nodeSources, expectedRevision, user, UPDATE_COMMIT_MESSAGE);
}

/**
* Saves an automation definition with an operation-specific project Git commit
* message. Package-owned operations such as version restore use this boundary so
* they retain the same locking, validation, and persistence behavior as an
* ordinary save.
*
* @param projectId project ID
* @param definitionJson canonical graph JSON
* @param nodeSources source by non-start node ID
* @param expectedRevision revision returned by the prior read
* @param user user performing the save
* @param commitMessage project Git commit message
* @return persisted graph and node sources
*/
static AutomationDefinitionService.DefinitionFiles saveDefinition(String projectId, String definitionJson,
Map<String, String> nodeSources, String expectedRevision, User user, String commitMessage) {
if (commitMessage == null || commitMessage.isBlank()) {
throw new IllegalArgumentException("Automation version commit message must be provided.");
}
ReentrantLock projectLock = ProjectSyncUtility.getProjectLock(projectId);
projectLock.lock();
try {
Expand All @@ -243,7 +276,7 @@ public static AutomationDefinitionService.DefinitionFiles saveDefinition(String
AutomationDefinitionService.DefinitionFiles files = AutomationDefinitionService.save(projectId,
definitionJson, nodeSources);
AutomationMcpSync.sync(projectId, files.definition(), user);
syncDefinitionAssets(projectId, user, "Update automation definition");
syncDefinitionAssets(projectId, user, commitMessage);
// Return the same normalized aggregate a subsequent GetAutomation call will
// read. This keeps the optimistic-concurrency token stable after Save.
return AutomationDefinitionService.load(projectId);
Expand Down Expand Up @@ -416,9 +449,8 @@ private static void syncDefinitionAssets(String projectId, User user, String com

private static void syncDefinitionAssets(IProject project, String projectId, User user, String commitMessage) {
if (project == null) {
classLogger.warn("Project {} not found in registry; skipping automation asset synchronization", projectId);
SecurityProjectUtils.updateProjectLastEditedDate(projectId);
return;
classLogger.error("Unable to version automation definition because project {} is not available", projectId);
throw new IllegalArgumentException("Automation project is unavailable for version history: " + projectId);
}

List<String> files = new ArrayList<>();
Expand All @@ -432,10 +464,10 @@ private static void syncDefinitionAssets(IProject project, String projectId, Use

String versionFolder = AssetUtility.getProjectVersionFolder(project.getProjectName(), projectId);
try {
GitRepoUtils.addSpecificFiles(versionFolder, files);
GitRepoUtils.commitAddedFiles(versionFolder, commitMessage, user);
} catch (Exception e) {
classLogger.warn("Git commit failed for automation project {}", projectId, e);
commitDefinitionAssets(versionFolder, files, commitMessage, user);
} catch (RuntimeException e) {
classLogger.error("Failed to version automation definition for project {}", projectId, e);
throw e;
}
if (ClusterUtil.IS_CLUSTER) {
try {
Expand All @@ -446,4 +478,74 @@ private static void syncDefinitionAssets(IProject project, String projectId, Use
}
SecurityProjectUtils.updateProjectLastEditedDate(projectId);
}

/**
* Stages and commits Automation-owned project assets through the project's
* standard Git repository. This operation uses JGit directly because the shared
* convenience helpers intentionally log and suppress add/commit API failures,
* while version history must report a failed save to its caller.
*
* @param versionFolder project Git working directory
* @param files Automation-owned absolute artifact paths
* @param commitMessage project Git commit message
* @param user commit author
*/
static void commitDefinitionAssets(String versionFolder, List<String> files, String commitMessage, User user) {
Set<String> ownedPaths = repositoryRelativePaths(versionFolder, files);
try (Git git = Git.open(new File(versionFolder))) {
AddCommand add = git.add();
ownedPaths.forEach(add::addFilepattern);
add.call();

Status status = git.status().call();
if (hasOwnedChanges(status.getModified(), ownedPaths) || hasOwnedChanges(status.getMissing(), ownedPaths)
|| hasOwnedChanges(status.getUntracked(), ownedPaths)
|| hasOwnedChanges(status.getConflicting(), ownedPaths)) {
throw new IllegalArgumentException("Unable to stage Automation definition assets for version history.");
}
boolean hasStagedChanges = hasOwnedChanges(status.getAdded(), ownedPaths)
|| hasOwnedChanges(status.getChanged(), ownedPaths)
|| hasOwnedChanges(status.getRemoved(), ownedPaths);
if (!hasStagedChanges) {
return;
}

AccessToken accessToken = user.getAccessToken(user.getPrimaryLogin());
String author = accessToken == null ? null : accessToken.getResolvedUsername();
String email = accessToken == null ? null : accessToken.getEmail();
git.commit().setMessage(commitMessage).setAuthor(defaultIfBlank(author, "SEMOSS"),
defaultIfBlank(email, "semoss@semoss.org")).call();
} catch (IOException | GitAPIException | NoWorkTreeException e) {
throw new IllegalArgumentException("Unable to commit Automation definition assets to version history.", e);
}
}

private static Set<String> repositoryRelativePaths(String versionFolder, List<String> files) {
Path repository = Path.of(versionFolder).toAbsolutePath().normalize();
Set<String> relativePaths = new HashSet<>();
for (String file : files) {
Path artifact = Path.of(file).toAbsolutePath().normalize();
if (!artifact.startsWith(repository)) {
throw new IllegalArgumentException("Automation definition asset is outside the project version folder.");
}
relativePaths.add(repository.relativize(artifact).toString().replace(File.separatorChar, '/'));
}
return relativePaths;
}

private static boolean hasOwnedChanges(Collection<String> changedPaths, Set<String> ownedPaths) {
for (String changedPath : changedPaths) {
String normalizedChangedPath = changedPath.replace('\\', '/');
for (String ownedPath : ownedPaths) {
if (ownedPath.equals(normalizedChangedPath) || ownedPath.startsWith(normalizedChangedPath + "/")) {
return true;
}
}
}
return false;
}

private static String defaultIfBlank(String value, String defaultValue) {
return value == null || value.isBlank() ? defaultValue : value;
}
}
Loading
Loading