diff --git a/src/main/java/com/simisinc/platform/application/cms/GitPublishCommand.java b/src/main/java/com/simisinc/platform/application/cms/GitPublishCommand.java new file mode 100644 index 00000000..5409f87a --- /dev/null +++ b/src/main/java/com/simisinc/platform/application/cms/GitPublishCommand.java @@ -0,0 +1,402 @@ +/* + * Copyright 2026 Matt Rajkowski (https://github.com/rajkowski) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.simisinc.platform.application.cms; + +import java.io.BufferedReader; +import java.io.File; +import java.io.IOException; +import java.io.InputStreamReader; +import java.text.SimpleDateFormat; +import java.util.Date; +import java.util.HashMap; +import java.util.Map; + +import org.apache.commons.io.FileUtils; +import org.apache.commons.lang3.StringUtils; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.node.ObjectNode; +import com.simisinc.platform.application.DataException; +import com.simisinc.platform.application.filesystem.FileSystemCommand; +import com.simisinc.platform.application.http.HttpPostCommand; +import com.simisinc.platform.application.json.JsonCommand; +import com.simisinc.platform.domain.model.cms.GitPublishSettings; + +/** + * Publishes static site content to a Git repository + * + * @author matt rajkowski + * @created 2/14/26 2:30 PM + */ +public class GitPublishCommand { + + private static Log LOG = LogFactory.getLog(GitPublishCommand.class); + + public static boolean publish(GitPublishSettings settings, String staticSiteZipPath) throws DataException { + + if (settings == null || !settings.getEnabled()) { + LOG.debug("Git publishing is not enabled"); + return false; + } + + if (StringUtils.isBlank(staticSiteZipPath)) { + throw new DataException("Static site zip path is required"); + } + + try { + // Create a temporary working directory + File tempDir = new File(System.getProperty("java.io.tmpdir"), "git-publish-" + System.currentTimeMillis()); + tempDir.mkdirs(); + + try { + // Clone the repository + cloneRepository(settings, tempDir); + + // Create or checkout the branch + checkoutBranch(settings, tempDir); + + // Extract the static site to the target directory + extractStaticSite(staticSiteZipPath, settings.getTargetDirectory(), tempDir); + + // Commit and push the changes + commitAndPush(settings, tempDir); + + // Create a pull request if configured + if (settings.getAutoCreatePr()) { + createPullRequest(settings); + } + + LOG.info("Successfully published static site to Git repository"); + return true; + + } finally { + // Clean up the temporary directory + try { + FileUtils.deleteDirectory(tempDir); + } catch (IOException e) { + LOG.warn("Could not delete temp directory: " + tempDir, e); + } + } + + } catch (Exception e) { + LOG.error("Error publishing to Git", e); + throw new DataException("Failed to publish to Git: " + e.getMessage()); + } + } + + private static void cloneRepository(GitPublishSettings settings, File workDir) throws Exception { + LOG.info("Cloning repository: " + settings.getRepositoryUrl()); + + // Use Git credential helper for authentication instead of embedding token in URL + // This is more secure as it avoids exposing tokens in logs + String repoUrl = settings.getRepositoryUrl(); + + // Execute git clone command without credentials in URL + ProcessBuilder pb = new ProcessBuilder("git", "clone", "--depth", "1", repoUrl, workDir.getAbsolutePath()); + + // Set up environment to pass credentials securely + if (settings.getAccessToken() != null) { + Map env = pb.environment(); + env.put("GIT_TERMINAL_PROMPT", "0"); // Disable interactive prompts + // For HTTPS, we'll configure credential helper in the cloned repo + } + + executeGitCommand(pb, "clone repository"); + + // Configure credential helper to use the access token + if (settings.getAccessToken() != null) { + configureCredentialHelper(settings, workDir); + } + } + + private static void checkoutBranch(GitPublishSettings settings, File workDir) throws Exception { + LOG.info("Checking out branch: " + settings.getBranchName()); + + // Try to checkout existing branch or create new one + try { + ProcessBuilder pb = new ProcessBuilder("git", "checkout", settings.getBranchName()); + pb.directory(workDir); + executeGitCommand(pb, "checkout branch"); + } catch (Exception e) { + // Branch doesn't exist, create it + LOG.info("Branch does not exist, creating new branch: " + settings.getBranchName()); + ProcessBuilder pb = new ProcessBuilder("git", "checkout", "-b", settings.getBranchName()); + pb.directory(workDir); + executeGitCommand(pb, "create branch"); + } + } + + private static void extractStaticSite(String zipPath, String targetDirectory, File workDir) throws Exception { + LOG.info("Extracting static site to: " + targetDirectory); + + // Get the zip file + File zipFile = new File(FileSystemCommand.getFileServerRootPathValue() + zipPath); + if (!zipFile.exists()) { + throw new DataException("Static site zip file not found: " + zipFile); + } + + // Determine the target directory within the git repo + File targetDir = new File(workDir, targetDirectory); + if (!targetDirectory.equals("/")) { + targetDir.mkdirs(); + } + + // Clear existing content in target directory (except .git) + if (targetDir.exists()) { + File[] files = targetDir.listFiles(); + if (files != null) { + for (File file : files) { + if (!file.getName().equals(".git")) { + if (file.isDirectory()) { + FileUtils.deleteDirectory(file); + } else { + file.delete(); + } + } + } + } + } + + // Unzip the static site content + ProcessBuilder pb = new ProcessBuilder("unzip", "-q", zipFile.getAbsolutePath(), "-d", targetDir.getAbsolutePath()); + executeGitCommand(pb, "extract static site"); + + // Move content from site/ subdirectory if it exists + File siteDir = new File(targetDir, "site"); + if (siteDir.exists() && siteDir.isDirectory()) { + File[] siteFiles = siteDir.listFiles(); + if (siteFiles != null) { + for (File file : siteFiles) { + File dest = new File(targetDir, file.getName()); + if (file.isDirectory()) { + FileUtils.moveDirectory(file, dest); + } else { + FileUtils.moveFile(file, dest); + } + } + } + siteDir.delete(); + } + } + + private static void commitAndPush(GitPublishSettings settings, File workDir) throws Exception { + LOG.info("Committing and pushing changes"); + + // Configure git user + ProcessBuilder pb1 = new ProcessBuilder("git", "config", "user.name", settings.getUsername()); + pb1.directory(workDir); + executeGitCommand(pb1, "configure git user.name"); + + ProcessBuilder pb2 = new ProcessBuilder("git", "config", "user.email", settings.getEmail()); + pb2.directory(workDir); + executeGitCommand(pb2, "configure git user.email"); + + // Add all changes + ProcessBuilder pb3 = new ProcessBuilder("git", "add", "."); + pb3.directory(workDir); + executeGitCommand(pb3, "add changes"); + + // Generate commit message from template + String commitMessage = generateMessage(settings.getCommitMessageTemplate()); + + // Commit changes + ProcessBuilder pb4 = new ProcessBuilder("git", "commit", "-m", commitMessage); + pb4.directory(workDir); + try { + executeGitCommand(pb4, "commit changes"); + } catch (Exception e) { + // Check if there are no changes to commit + if (e.getMessage() != null && e.getMessage().contains("nothing to commit")) { + LOG.info("No changes to commit"); + return; + } + throw e; + } + + // Push changes using the configured remote (credentials already set up via credential helper) + ProcessBuilder pb5 = new ProcessBuilder("git", "push", "origin", settings.getBranchName()); + pb5.directory(workDir); + executeGitCommand(pb5, "push changes"); + } + + private static void configureCredentialHelper(GitPublishSettings settings, File workDir) throws Exception { + // Use Git credential store to securely provide the access token + // This avoids embedding the token in URLs or command-line arguments + if (settings.getAccessToken() == null) { + return; + } + + // Configure git to use credential helper + ProcessBuilder pb1 = new ProcessBuilder("git", "config", "credential.helper", "store"); + pb1.directory(workDir); + executeGitCommand(pb1, "configure credential helper"); + + // Extract the host from the repository URL + String repoUrl = settings.getRepositoryUrl(); + String host = extractHostFromUrl(repoUrl); + + if (host != null) { + // Write credentials to the Git credential store + // Format: https://username:token@hostname + File credentialFile = new File(workDir, ".git-credentials"); + String credentialLine = "https://" + settings.getUsername() + ":" + settings.getAccessToken() + "@" + host; + try (java.io.FileWriter fw = new java.io.FileWriter(credentialFile)) { + fw.write(credentialLine + "\n"); + } + + // Point Git to this credential file + ProcessBuilder pb2 = new ProcessBuilder("git", "config", "credential.helper", "store --file=" + credentialFile.getAbsolutePath()); + pb2.directory(workDir); + executeGitCommand(pb2, "configure credential store path"); + } + } + + private static String extractHostFromUrl(String url) { + try { + // Extract hostname from URL + // Examples: https://github.com/user/repo.git -> github.com + // https://gitlab.com/user/repo.git -> gitlab.com + if (url.startsWith("https://")) { + String withoutProtocol = url.substring(8); // Remove "https://" + int slashIndex = withoutProtocol.indexOf('/'); + if (slashIndex > 0) { + return withoutProtocol.substring(0, slashIndex); + } + } + } catch (Exception e) { + LOG.warn("Failed to extract host from URL: " + url, e); + } + return null; + } + + private static void createPullRequest(GitPublishSettings settings) throws Exception { + LOG.info("Creating pull request"); + + String provider = settings.getGitProvider().toLowerCase(); + if ("github".equals(provider)) { + createGitHubPullRequest(settings); + } else if ("gitlab".equals(provider)) { + createGitLabMergeRequest(settings); + } else { + LOG.warn("Pull request creation not supported for provider: " + provider); + } + } + + private static void createGitHubPullRequest(GitPublishSettings settings) throws Exception { + // Extract owner and repo from URL + // Format: https://github.com/owner/repo.git + String repoUrl = settings.getRepositoryUrl(); + String[] parts = repoUrl.replace("https://github.com/", "").replace(".git", "").split("/"); + if (parts.length < 2) { + throw new DataException("Invalid GitHub repository URL"); + } + String owner = parts[0]; + String repo = parts[1]; + + // Prepare API request + String apiUrl = "https://api.github.com/repos/" + owner + "/" + repo + "/pulls"; + + ObjectMapper mapper = new ObjectMapper(); + ObjectNode body = mapper.createObjectNode(); + body.put("title", generateMessage(settings.getPrTitleTemplate())); + body.put("body", generateMessage(settings.getPrDescriptionTemplate())); + body.put("head", settings.getBranchName()); + body.put("base", settings.getBaseBranch()); + + Map headers = new HashMap<>(); + headers.put("Authorization", "Bearer " + settings.getAccessToken()); + headers.put("Accept", "application/vnd.github+json"); + headers.put("Content-Type", "application/json"); + + String response = HttpPostCommand.execute(apiUrl, headers, body.toString()); + if (StringUtils.isBlank(response)) { + throw new DataException("Failed to create pull request: empty response"); + } + + JsonNode responseJson = JsonCommand.fromString(response); + if (responseJson.has("html_url")) { + LOG.info("Pull request created: " + responseJson.get("html_url").asText()); + } else if (responseJson.has("message")) { + LOG.warn("GitHub API response: " + responseJson.get("message").asText()); + } + } + + private static void createGitLabMergeRequest(GitPublishSettings settings) throws Exception { + // Extract project path from URL + // Format: https://gitlab.com/owner/repo.git + String repoUrl = settings.getRepositoryUrl(); + String projectPath = repoUrl.replace("https://gitlab.com/", "").replace(".git", ""); + String encodedPath = projectPath.replace("/", "%2F"); + + // Prepare API request + String apiUrl = "https://gitlab.com/api/v4/projects/" + encodedPath + "/merge_requests"; + + ObjectMapper mapper = new ObjectMapper(); + ObjectNode body = mapper.createObjectNode(); + body.put("title", generateMessage(settings.getPrTitleTemplate())); + body.put("description", generateMessage(settings.getPrDescriptionTemplate())); + body.put("source_branch", settings.getBranchName()); + body.put("target_branch", settings.getBaseBranch()); + + Map headers = new HashMap<>(); + headers.put("PRIVATE-TOKEN", settings.getAccessToken()); + headers.put("Content-Type", "application/json"); + + String response = HttpPostCommand.execute(apiUrl, headers, body.toString()); + if (StringUtils.isBlank(response)) { + throw new DataException("Failed to create merge request: empty response"); + } + + JsonNode responseJson = JsonCommand.fromString(response); + if (responseJson.has("web_url")) { + LOG.info("Merge request created: " + responseJson.get("web_url").asText()); + } else if (responseJson.has("message")) { + LOG.warn("GitLab API response: " + responseJson.get("message").asText()); + } + } + + private static String generateMessage(String template) { + if (StringUtils.isBlank(template)) { + return "Static site update"; + } + SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); + String timestamp = sdf.format(new Date()); + return template.replace("${timestamp}", timestamp); + } + + private static void executeGitCommand(ProcessBuilder pb, String description) throws Exception { + pb.redirectErrorStream(true); + Process process = pb.start(); + + StringBuilder output = new StringBuilder(); + try (BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream()))) { + String line; + while ((line = reader.readLine()) != null) { + output.append(line).append("\n"); + LOG.debug(line); + } + } + + int exitCode = process.waitFor(); + if (exitCode != 0) { + throw new Exception("Failed to " + description + " (exit code: " + exitCode + "): " + output); + } + } +} diff --git a/src/main/java/com/simisinc/platform/application/cms/LoadGitPublishSettingsCommand.java b/src/main/java/com/simisinc/platform/application/cms/LoadGitPublishSettingsCommand.java new file mode 100644 index 00000000..3a28c70f --- /dev/null +++ b/src/main/java/com/simisinc/platform/application/cms/LoadGitPublishSettingsCommand.java @@ -0,0 +1,38 @@ +/* + * Copyright 2026 Matt Rajkowski (https://github.com/rajkowski) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.simisinc.platform.application.cms; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +import com.simisinc.platform.domain.model.cms.GitPublishSettings; +import com.simisinc.platform.infrastructure.persistence.cms.GitPublishSettingsRepository; + +/** + * Loads Git publish settings from the database + * + * @author matt rajkowski + * @created 2/14/26 2:15 PM + */ +public class LoadGitPublishSettingsCommand { + + private static Log LOG = LogFactory.getLog(LoadGitPublishSettingsCommand.class); + + public static GitPublishSettings loadSettings() { + return GitPublishSettingsRepository.findSettings(); + } +} diff --git a/src/main/java/com/simisinc/platform/application/cms/SaveGitPublishSettingsCommand.java b/src/main/java/com/simisinc/platform/application/cms/SaveGitPublishSettingsCommand.java new file mode 100644 index 00000000..d4a821b7 --- /dev/null +++ b/src/main/java/com/simisinc/platform/application/cms/SaveGitPublishSettingsCommand.java @@ -0,0 +1,68 @@ +/* + * Copyright 2026 Matt Rajkowski (https://github.com/rajkowski) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.simisinc.platform.application.cms; + +import org.apache.commons.lang3.StringUtils; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +import com.simisinc.platform.application.DataException; +import com.simisinc.platform.domain.model.cms.GitPublishSettings; +import com.simisinc.platform.infrastructure.persistence.cms.GitPublishSettingsRepository; + +/** + * Saves Git publish settings to the database + * + * @author matt rajkowski + * @created 2/14/26 2:15 PM + */ +public class SaveGitPublishSettingsCommand { + + private static Log LOG = LogFactory.getLog(SaveGitPublishSettingsCommand.class); + + public static GitPublishSettings saveSettings(GitPublishSettings settingsBean) throws DataException { + + // Determine if this is a new record + boolean isNewRecord = settingsBean.getId() <= 0; + + // Required fields + if (settingsBean.getEnabled()) { + if (StringUtils.isBlank(settingsBean.getGitProvider())) { + throw new DataException("Git provider is required"); + } + if (StringUtils.isBlank(settingsBean.getRepositoryUrl())) { + throw new DataException("Repository URL is required"); + } + if (StringUtils.isBlank(settingsBean.getBranchName())) { + throw new DataException("Branch name is required"); + } + // Access token is required only for new records or when being updated + if (isNewRecord && StringUtils.isBlank(settingsBean.getAccessToken())) { + throw new DataException("Access token is required"); + } + if (StringUtils.isBlank(settingsBean.getUsername())) { + throw new DataException("Username is required"); + } + if (StringUtils.isBlank(settingsBean.getEmail())) { + throw new DataException("Email is required"); + } + } + + // Save the settings + return GitPublishSettingsRepository.save(settingsBean); + } +} diff --git a/src/main/java/com/simisinc/platform/domain/model/cms/GitPublishSettings.java b/src/main/java/com/simisinc/platform/domain/model/cms/GitPublishSettings.java new file mode 100644 index 00000000..8ece3520 --- /dev/null +++ b/src/main/java/com/simisinc/platform/domain/model/cms/GitPublishSettings.java @@ -0,0 +1,198 @@ +/* + * Copyright 2026 Matt Rajkowski (https://github.com/rajkowski) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.simisinc.platform.domain.model.cms; + +import java.sql.Timestamp; + +import com.simisinc.platform.domain.model.Entity; + +/** + * Git publish settings for static site export + * + * @author matt rajkowski + * @created 2/14/26 2:00 PM + */ +public class GitPublishSettings extends Entity { + + private Long id = -1L; + + private boolean enabled = false; + private String gitProvider = null; + private String repositoryUrl = null; + private String branchName = "main"; + private String baseBranch = "main"; + private String accessToken = null; + private String username = null; + private String email = null; + private String commitMessageTemplate = "Static site update: ${timestamp}"; + private boolean autoCreatePr = true; + private String prTitleTemplate = "Static site update: ${timestamp}"; + private String prDescriptionTemplate = "Automated static site export"; + private String targetDirectory = "/"; + + private Timestamp created = null; + private Timestamp modified = null; + private long createdBy = -1L; + private long modifiedBy = -1L; + + public GitPublishSettings() { + } + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public boolean getEnabled() { + return enabled; + } + + public void setEnabled(boolean enabled) { + this.enabled = enabled; + } + + public String getGitProvider() { + return gitProvider; + } + + public void setGitProvider(String gitProvider) { + this.gitProvider = gitProvider; + } + + public String getRepositoryUrl() { + return repositoryUrl; + } + + public void setRepositoryUrl(String repositoryUrl) { + this.repositoryUrl = repositoryUrl; + } + + public String getBranchName() { + return branchName; + } + + public void setBranchName(String branchName) { + this.branchName = branchName; + } + + public String getBaseBranch() { + return baseBranch; + } + + public void setBaseBranch(String baseBranch) { + this.baseBranch = baseBranch; + } + + public String getAccessToken() { + return accessToken; + } + + public void setAccessToken(String accessToken) { + this.accessToken = accessToken; + } + + public String getUsername() { + return username; + } + + public void setUsername(String username) { + this.username = username; + } + + public String getEmail() { + return email; + } + + public void setEmail(String email) { + this.email = email; + } + + public String getCommitMessageTemplate() { + return commitMessageTemplate; + } + + public void setCommitMessageTemplate(String commitMessageTemplate) { + this.commitMessageTemplate = commitMessageTemplate; + } + + public boolean getAutoCreatePr() { + return autoCreatePr; + } + + public void setAutoCreatePr(boolean autoCreatePr) { + this.autoCreatePr = autoCreatePr; + } + + public String getPrTitleTemplate() { + return prTitleTemplate; + } + + public void setPrTitleTemplate(String prTitleTemplate) { + this.prTitleTemplate = prTitleTemplate; + } + + public String getPrDescriptionTemplate() { + return prDescriptionTemplate; + } + + public void setPrDescriptionTemplate(String prDescriptionTemplate) { + this.prDescriptionTemplate = prDescriptionTemplate; + } + + public String getTargetDirectory() { + return targetDirectory; + } + + public void setTargetDirectory(String targetDirectory) { + this.targetDirectory = targetDirectory; + } + + public Timestamp getCreated() { + return created; + } + + public void setCreated(Timestamp created) { + this.created = created; + } + + public Timestamp getModified() { + return modified; + } + + public void setModified(Timestamp modified) { + this.modified = modified; + } + + public long getCreatedBy() { + return createdBy; + } + + public void setCreatedBy(long createdBy) { + this.createdBy = createdBy; + } + + public long getModifiedBy() { + return modifiedBy; + } + + public void setModifiedBy(long modifiedBy) { + this.modifiedBy = modifiedBy; + } +} diff --git a/src/main/java/com/simisinc/platform/infrastructure/persistence/cms/GitPublishSettingsRepository.java b/src/main/java/com/simisinc/platform/infrastructure/persistence/cms/GitPublishSettingsRepository.java new file mode 100644 index 00000000..78aea94c --- /dev/null +++ b/src/main/java/com/simisinc/platform/infrastructure/persistence/cms/GitPublishSettingsRepository.java @@ -0,0 +1,144 @@ +/* + * Copyright 2026 Matt Rajkowski (https://github.com/rajkowski) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.simisinc.platform.infrastructure.persistence.cms; + +import java.sql.Connection; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.sql.Timestamp; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +import com.simisinc.platform.domain.model.cms.GitPublishSettings; +import com.simisinc.platform.infrastructure.database.DB; +import com.simisinc.platform.infrastructure.database.SqlUtils; + +/** + * Persists and retrieves Git publish settings objects + * + * @author matt rajkowski + * @created 2/14/26 2:00 PM + */ +public class GitPublishSettingsRepository { + + private static Log LOG = LogFactory.getLog(GitPublishSettingsRepository.class); + + private static String TABLE_NAME = "git_publish_settings"; + private static String[] PRIMARY_KEY = new String[] { "settings_id" }; + + public static GitPublishSettings findSettings() { + return (GitPublishSettings) DB.selectRecordFrom( + TABLE_NAME, + null, + GitPublishSettingsRepository::buildRecord); + } + + public static GitPublishSettings save(GitPublishSettings record) { + if (record.getId() > -1) { + return update(record); + } + return add(record); + } + + private static GitPublishSettings add(GitPublishSettings record) { + SqlUtils insertValues = new SqlUtils() + .add("enabled", record.getEnabled()) + .add("git_provider", record.getGitProvider()) + .add("repository_url", record.getRepositoryUrl()) + .add("branch_name", record.getBranchName()) + .add("base_branch", record.getBaseBranch()) + .add("access_token", record.getAccessToken()) + .add("username", record.getUsername()) + .add("email", record.getEmail()) + .add("commit_message_template", record.getCommitMessageTemplate()) + .add("auto_create_pr", record.getAutoCreatePr()) + .add("pr_title_template", record.getPrTitleTemplate()) + .add("pr_description_template", record.getPrDescriptionTemplate()) + .add("target_directory", record.getTargetDirectory()) + .add("created_by", record.getCreatedBy(), -1) + .add("modified_by", record.getModifiedBy(), -1); + record.setId(DB.insertInto(TABLE_NAME, insertValues, PRIMARY_KEY)); + if (record.getId() == -1) { + LOG.error("An id was not set!"); + return null; + } + return record; + } + + private static GitPublishSettings update(GitPublishSettings record) { + SqlUtils updateValues = new SqlUtils() + .add("enabled", record.getEnabled()) + .add("git_provider", record.getGitProvider()) + .add("repository_url", record.getRepositoryUrl()) + .add("branch_name", record.getBranchName()) + .add("base_branch", record.getBaseBranch()) + .add("access_token", record.getAccessToken()) + .add("username", record.getUsername()) + .add("email", record.getEmail()) + .add("commit_message_template", record.getCommitMessageTemplate()) + .add("auto_create_pr", record.getAutoCreatePr()) + .add("pr_title_template", record.getPrTitleTemplate()) + .add("pr_description_template", record.getPrDescriptionTemplate()) + .add("target_directory", record.getTargetDirectory()) + .add("modified", new Timestamp(System.currentTimeMillis())) + .add("modified_by", record.getModifiedBy(), -1); + if (DB.update(TABLE_NAME, updateValues, DB.WHERE("settings_id = ?", record.getId()))) { + return record; + } + LOG.error("The update failed!"); + return null; + } + + public static boolean remove(GitPublishSettings record) { + try (Connection connection = DB.getConnection()) { + DB.deleteFrom(connection, TABLE_NAME, DB.WHERE("settings_id = ?", record.getId())); + return true; + } catch (SQLException se) { + LOG.error("SQLException: " + se.getMessage()); + } + return false; + } + + private static GitPublishSettings buildRecord(ResultSet rs) { + try { + GitPublishSettings record = new GitPublishSettings(); + record.setId(rs.getLong("settings_id")); + record.setEnabled(rs.getBoolean("enabled")); + record.setGitProvider(rs.getString("git_provider")); + record.setRepositoryUrl(rs.getString("repository_url")); + record.setBranchName(rs.getString("branch_name")); + record.setBaseBranch(rs.getString("base_branch")); + record.setAccessToken(rs.getString("access_token")); + record.setUsername(rs.getString("username")); + record.setEmail(rs.getString("email")); + record.setCommitMessageTemplate(rs.getString("commit_message_template")); + record.setAutoCreatePr(rs.getBoolean("auto_create_pr")); + record.setPrTitleTemplate(rs.getString("pr_title_template")); + record.setPrDescriptionTemplate(rs.getString("pr_description_template")); + record.setTargetDirectory(rs.getString("target_directory")); + record.setCreated(rs.getTimestamp("created")); + record.setModified(rs.getTimestamp("modified")); + record.setCreatedBy(DB.getLong(rs, "created_by", -1)); + record.setModifiedBy(DB.getLong(rs, "modified_by", -1)); + return record; + } catch (SQLException se) { + LOG.error("buildRecord", se); + return null; + } + } +} diff --git a/src/main/java/com/simisinc/platform/infrastructure/scheduler/cms/MakeStaticSiteJob.java b/src/main/java/com/simisinc/platform/infrastructure/scheduler/cms/MakeStaticSiteJob.java index af4ec4b0..dffae3af 100644 --- a/src/main/java/com/simisinc/platform/infrastructure/scheduler/cms/MakeStaticSiteJob.java +++ b/src/main/java/com/simisinc/platform/infrastructure/scheduler/cms/MakeStaticSiteJob.java @@ -31,8 +31,11 @@ import org.thymeleaf.templateresolver.WebApplicationTemplateResolver; import org.thymeleaf.web.servlet.JavaxServletWebApplication; +import com.simisinc.platform.application.cms.GitPublishCommand; +import com.simisinc.platform.application.cms.LoadGitPublishSettingsCommand; import com.simisinc.platform.application.cms.MakeStaticSiteCommand; import com.simisinc.platform.application.cms.WebPageXmlLayoutCommand; +import com.simisinc.platform.domain.model.cms.GitPublishSettings; import com.simisinc.platform.infrastructure.distributedlock.LockManager; import com.simisinc.platform.infrastructure.scheduler.SchedulerManager; import com.simisinc.platform.presentation.controller.PageTemplateEngine; @@ -87,6 +90,24 @@ public static void execute() { String filePath = MakeStaticSiteCommand.execute(templateEngineProperties); if (filePath != null) { LOG.info("Static site generated at: " + filePath); + + // Check if Git publishing is enabled + GitPublishSettings gitSettings = LoadGitPublishSettingsCommand.loadSettings(); + if (gitSettings != null && gitSettings.getEnabled()) { + LOG.info("Git publishing is enabled, attempting to publish to repository..."); + try { + boolean published = GitPublishCommand.publish(gitSettings, filePath); + if (published) { + LOG.info("Successfully published static site to Git repository"); + } else { + LOG.warn("Git publishing skipped or failed"); + } + } catch (Exception e) { + LOG.error("Error publishing to Git repository", e); + } + } else { + LOG.debug("Git publishing is not enabled"); + } } else { LOG.error("Static site generation failed."); } diff --git a/src/main/java/com/simisinc/platform/presentation/widgets/editor/StaticSiteEndpoint.java b/src/main/java/com/simisinc/platform/presentation/widgets/editor/StaticSiteEndpoint.java index 838c07aa..9f7465bc 100644 --- a/src/main/java/com/simisinc/platform/presentation/widgets/editor/StaticSiteEndpoint.java +++ b/src/main/java/com/simisinc/platform/presentation/widgets/editor/StaticSiteEndpoint.java @@ -26,8 +26,12 @@ import org.jobrunr.scheduling.BackgroundJobRequest; +import com.simisinc.platform.application.DataException; +import com.simisinc.platform.application.cms.LoadGitPublishSettingsCommand; import com.simisinc.platform.application.cms.MakeStaticSiteCommand; +import com.simisinc.platform.application.cms.SaveGitPublishSettingsCommand; import com.simisinc.platform.application.json.JsonCommand; +import com.simisinc.platform.domain.model.cms.GitPublishSettings; import com.simisinc.platform.infrastructure.scheduler.cms.MakeStaticSiteJob; import com.simisinc.platform.presentation.controller.WidgetContext; import com.simisinc.platform.presentation.widgets.GenericWidget; @@ -70,6 +74,9 @@ public WidgetContext action(WidgetContext context) { } else if ("LIST".equalsIgnoreCase(action)) { // The user is listing the files return list(context); + } else if ("GET_GIT_SETTINGS".equalsIgnoreCase(action)) { + // The user is getting Git settings + return getGitSettings(context); } // No default action LOG.error("Unknown action: " + action); @@ -100,6 +107,8 @@ public WidgetContext post(WidgetContext context) throws InvocationTargetExceptio return generate(context); } else if ("delete".equals(action)) { return delete(context); + } else if ("saveGitSettings".equals(action)) { + return saveGitSettings(context); } } catch (IOException e) { LOG.error("Error in StaticSiteEndpoint", e); @@ -238,4 +247,139 @@ private void sendSuccessResponse(HttpServletResponse response) throws IOExceptio private void sendErrorResponse(HttpServletResponse response, String message) throws IOException { sendJsonResponse(response, "{\"status\":\"error\", \"message\":\"" + message + "\"}"); } + + /** Get Git publish settings */ + private WidgetContext getGitSettings(WidgetContext context) throws IOException { + LOG.debug("Getting Git publish settings..."); + + GitPublishSettings settings = LoadGitPublishSettingsCommand.loadSettings(); + Map result = new HashMap<>(); + + if (settings != null) { + result.put("enabled", settings.getEnabled()); + result.put("gitProvider", settings.getGitProvider()); + result.put("repositoryUrl", settings.getRepositoryUrl()); + result.put("branchName", settings.getBranchName()); + result.put("baseBranch", settings.getBaseBranch()); + result.put("username", settings.getUsername()); + result.put("email", settings.getEmail()); + result.put("commitMessageTemplate", settings.getCommitMessageTemplate()); + result.put("autoCreatePr", settings.getAutoCreatePr()); + result.put("prTitleTemplate", settings.getPrTitleTemplate()); + result.put("prDescriptionTemplate", settings.getPrDescriptionTemplate()); + result.put("targetDirectory", settings.getTargetDirectory()); + // Don't send the access token to the client + } else { + // Return default values + result.put("enabled", false); + result.put("gitProvider", "github"); + result.put("branchName", "main"); + result.put("baseBranch", "main"); + result.put("commitMessageTemplate", "Static site update: ${timestamp}"); + result.put("autoCreatePr", true); + result.put("prTitleTemplate", "Static site update: ${timestamp}"); + result.put("prDescriptionTemplate", "Automated static site export"); + result.put("targetDirectory", "/"); + } + + String json = JsonCommand.createJsonNode(result).toString(); + context.setJson(json); + context.setSuccess(true); + return context; + } + + /** Save Git publish settings */ + private WidgetContext saveGitSettings(WidgetContext context) throws IOException { + LOG.debug("Saving Git publish settings..."); + + try { + // Load existing settings or create new + GitPublishSettings settings = LoadGitPublishSettingsCommand.loadSettings(); + if (settings == null) { + settings = new GitPublishSettings(); + settings.setCreatedBy(context.getUserId()); + } + settings.setModifiedBy(context.getUserId()); + + // Update from request parameters + String enabledParam = context.getParameter("enabled"); + settings.setEnabled("true".equals(enabledParam)); + + String gitProvider = context.getParameter("gitProvider"); + if (gitProvider != null) { + settings.setGitProvider(gitProvider); + } + + String repositoryUrl = context.getParameter("repositoryUrl"); + if (repositoryUrl != null) { + settings.setRepositoryUrl(repositoryUrl.trim()); + } + + String branchName = context.getParameter("branchName"); + if (branchName != null) { + settings.setBranchName(branchName.trim()); + } + + String baseBranch = context.getParameter("baseBranch"); + if (baseBranch != null) { + settings.setBaseBranch(baseBranch.trim()); + } + + String accessToken = context.getParameter("accessToken"); + if (accessToken != null && !accessToken.trim().isEmpty()) { + settings.setAccessToken(accessToken.trim()); + } + + String username = context.getParameter("username"); + if (username != null) { + settings.setUsername(username.trim()); + } + + String email = context.getParameter("email"); + if (email != null) { + settings.setEmail(email.trim()); + } + + String commitMessageTemplate = context.getParameter("commitMessageTemplate"); + if (commitMessageTemplate != null) { + settings.setCommitMessageTemplate(commitMessageTemplate.trim()); + } + + String autoCreatePr = context.getParameter("autoCreatePr"); + settings.setAutoCreatePr("true".equals(autoCreatePr)); + + String prTitleTemplate = context.getParameter("prTitleTemplate"); + if (prTitleTemplate != null) { + settings.setPrTitleTemplate(prTitleTemplate.trim()); + } + + String prDescriptionTemplate = context.getParameter("prDescriptionTemplate"); + if (prDescriptionTemplate != null) { + settings.setPrDescriptionTemplate(prDescriptionTemplate.trim()); + } + + String targetDirectory = context.getParameter("targetDirectory"); + if (targetDirectory != null) { + settings.setTargetDirectory(targetDirectory.trim()); + } + + // Save the settings + SaveGitPublishSettingsCommand.saveSettings(settings); + + sendSuccessResponse(context.getResponse()); + context.setHandledResponse(true); + return context; + + } catch (DataException e) { + LOG.error("Data exception saving Git settings", e); + sendErrorResponse(context.getResponse(), e.getMessage()); + context.setHandledResponse(true); + return context; + } catch (Exception e) { + LOG.error("Error saving Git settings", e); + sendErrorResponse(context.getResponse(), "Failed to save settings: " + e.getMessage()); + context.setHandledResponse(true); + return context; + } + } } \ No newline at end of file diff --git a/src/main/resources/database/upgrade/2026/UPGRADE_20260214.1000__git_publish_settings.sql b/src/main/resources/database/upgrade/2026/UPGRADE_20260214.1000__git_publish_settings.sql new file mode 100644 index 00000000..e243f75c --- /dev/null +++ b/src/main/resources/database/upgrade/2026/UPGRADE_20260214.1000__git_publish_settings.sql @@ -0,0 +1,23 @@ +-- Copyright 2026 Matt Rajkowski, Licensed under the Apache License, Version 2.0 +-- Git publish settings for static site export + +CREATE TABLE git_publish_settings ( + settings_id BIGSERIAL PRIMARY KEY, + enabled BOOLEAN DEFAULT false, + git_provider VARCHAR(50) NOT NULL, + repository_url VARCHAR(500) NOT NULL, + branch_name VARCHAR(255) DEFAULT 'main', + base_branch VARCHAR(255) DEFAULT 'main', + access_token TEXT, + username VARCHAR(255), + email VARCHAR(255), + commit_message_template VARCHAR(500) DEFAULT 'Static site update: ${timestamp}', + auto_create_pr BOOLEAN DEFAULT true, + pr_title_template VARCHAR(255) DEFAULT 'Static site update: ${timestamp}', + pr_description_template TEXT DEFAULT 'Automated static site export', + target_directory VARCHAR(500) DEFAULT '/', + created TIMESTAMP(3) DEFAULT CURRENT_TIMESTAMP, + modified TIMESTAMP(3) DEFAULT CURRENT_TIMESTAMP, + created_by BIGINT REFERENCES users(user_id), + modified_by BIGINT REFERENCES users(user_id) +); diff --git a/src/main/webapp/WEB-INF/jsp/cms/static-site-modal.jsp b/src/main/webapp/WEB-INF/jsp/cms/static-site-modal.jsp index fdda487d..5050ead7 100644 --- a/src/main/webapp/WEB-INF/jsp/cms/static-site-modal.jsp +++ b/src/main/webapp/WEB-INF/jsp/cms/static-site-modal.jsp @@ -16,9 +16,64 @@ <%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %> <%@ taglib prefix="font" uri="/WEB-INF/tlds/font-functions.tld" %>