From f8c9113cd45ff70a12bd22815099cda7303cdfa1 Mon Sep 17 00:00:00 2001 From: Naveen Kumar Date: Tue, 26 May 2026 13:22:29 +0530 Subject: [PATCH 1/2] RTECO-858: Add JFrog CLI Task support for Bamboo Deployment Projects MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit JfTask implements TaskType only and is therefore invisible in the Deployment Project task picker. Bamboo requires DeploymentTaskType to be implemented for a task to appear there. Add JfDeploymentTask which: - Extends JfContext (inheriting AbstractTaskConfigurator) so the same configuration UI and FTL template (editJfTask.ftl) are reused - Implements DeploymentTaskType so it appears in the Deployment Project task picker - Reuses JfInstaller, ExecutableRunner, and ServerConfigManager — the full jf CLI download, config-add, and command-run flow is identical to JfTask - Adapts environment variable setup for the deployment context: build-specific vars (JFROG_CLI_BUILD_NAME, JFROG_CLI_BUILD_NUMBER, JFROG_CLI_BUILD_URL) are omitted since no BuildContext is available; JFROG_CLI_HOME_DIR is isolated per deployment run using the deployment result ID Register the new task in atlassian-plugin.xml with category "deployment". Co-authored-by: Cursor --- .../org/jfrog/bamboo/JfDeploymentTask.java | 212 ++++++++++++++++++ src/main/resources/atlassian-plugin.xml | 13 ++ 2 files changed, 225 insertions(+) create mode 100644 src/main/java/org/jfrog/bamboo/JfDeploymentTask.java diff --git a/src/main/java/org/jfrog/bamboo/JfDeploymentTask.java b/src/main/java/org/jfrog/bamboo/JfDeploymentTask.java new file mode 100644 index 0000000..99ec8d7 --- /dev/null +++ b/src/main/java/org/jfrog/bamboo/JfDeploymentTask.java @@ -0,0 +1,212 @@ +package org.jfrog.bamboo; + +import com.atlassian.bamboo.configuration.AdministrationConfigurationAccessor; +import com.atlassian.bamboo.deployments.execution.DeploymentTaskContext; +import com.atlassian.bamboo.deployments.execution.DeploymentTaskType; +import com.atlassian.bamboo.task.TaskResult; +import com.atlassian.bamboo.task.TaskResultBuilder; +import com.atlassian.bamboo.variable.CustomVariableContext; +import com.atlassian.plugin.PluginAccessor; +import com.atlassian.plugin.spring.scanner.annotation.imports.ComponentImport; +import org.apache.commons.lang.StringUtils; +import org.apache.commons.lang.exception.ExceptionUtils; +import org.jetbrains.annotations.NotNull; +import org.jfrog.bamboo.config.ServerConfig; +import org.jfrog.bamboo.config.ServerConfigManager; +import org.jfrog.bamboo.utils.BambooUtils; +import org.jfrog.bamboo.utils.BuildLog; +import org.jfrog.bamboo.utils.ExecutableRunner; +import org.jfrog.bamboo.utils.Utils; + +import javax.inject.Inject; +import java.io.File; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Paths; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +/** + * JFrog CLI Task for Bamboo Deployment Projects. + * + *

Implements {@link DeploymentTaskType} so it appears in the Deployment Project task picker, + * where the plain {@link JfTask} (which only implements {@link com.atlassian.bamboo.task.TaskType}) + * is invisible. + * + *

Shares its configurator ({@link JfContext}) and FTL template with {@link JfTask}, so the + * user-facing configuration UI is identical in both build plans and deployment projects. + */ +public class JfDeploymentTask extends JfContext implements DeploymentTaskType { + + private BuildLog buildLog; + + @Inject + private ServerConfigManager serverConfigManager; + + @Inject + @ComponentImport + private CustomVariableContext customVariableContext; + + @Inject + @ComponentImport + private PluginAccessor pluginAccessor; + + @Inject + @ComponentImport + private AdministrationConfigurationAccessor administrationConfigurationAccessor; + + /** + * Executes the JFrog CLI command in a Deployment Project environment. + */ + @Override + public @NotNull TaskResult execute(@NotNull DeploymentTaskContext taskContext) { + buildLog = new BuildLog(taskContext.getBuildLogger()); + TaskResultBuilder resultBuilder = TaskResultBuilder.newBuilder(taskContext); + + String selectedServerId = taskContext.getConfigurationMap().get(JF_TASK_SERVER_ID); + ServerConfig selectedServerConfig = serverConfigManager.getServerConfigById(selectedServerId); + if (selectedServerConfig == null) { + buildLog.error("The selected Server ID doesn't exist: " + selectedServerId); + return resultBuilder.failedWithError().build(); + } + + try { + String jfrogTmpDir = BambooUtils.getJfrogTmpDir(customVariableContext); + + // Download CLI (if needed) and retrieve path + String jfExecutablePath = JfInstaller.getJfExecutable(selectedServerConfig, jfrogTmpDir, buildLog); + + Map envs = createDeploymentEnvironmentVariables(taskContext, selectedServerConfig); + String customWd = taskContext.getConfigurationMap().get(JF_TASK_WORKING_DIRECTORY); + File workingDir = getWorkingDirectory(customWd, taskContext.getWorkingDirectory()); + buildLog.info("Working directory: " + workingDir); + + List secrets = List.of(selectedServerConfig.getPassword(), selectedServerConfig.getAccessToken()); + ExecutableRunner commandRunner = new ExecutableRunner(jfExecutablePath, workingDir, envs, secrets, buildLog); + + // Run 'jf config add' for every configured JFrog server + for (ServerConfig serverConfig : serverConfigManager.getAllServerConfigs()) { + int exitCode = runConfigAdd(commandRunner, serverConfig); + if (exitCode != 0) { + return resultBuilder.failedWithError().build(); + } + } + + // Make the selected server the default + int exitCode = commandRunner.run(List.of("config", "use", selectedServerConfig.getServerId())); + if (exitCode != 0) { + return resultBuilder.failedWithError().build(); + } + + // Run the user-supplied CLI command + String cliCommand = taskContext.getConfigurationMap().get(JF_TASK_COMMAND); + cliCommand = StringUtils.removeStart(cliCommand, "jf "); + + List unwrappedArgs = Utils.splitStringPreservingQuotes(cliCommand) + .stream() + .map(Utils::unQuote) + .collect(Collectors.toList()); + + exitCode = commandRunner.run(unwrappedArgs); + if (exitCode != 0) { + return resultBuilder.failedWithError().build(); + } + } catch (Exception e) { + buildLog.error(ExceptionUtils.getRootCauseMessage(e), e); + return resultBuilder.failedWithError().build(); + } + + return resultBuilder.success().build(); + } + + /** + * Builds the JFrog CLI environment variables for a deployment-project context. + * + *

Unlike the build-plan variant, there is no {@link com.atlassian.bamboo.v2.build.BuildContext} + * available here. Build-specific variables ({@code JFROG_CLI_BUILD_NAME}, + * {@code JFROG_CLI_BUILD_NUMBER}, {@code JFROG_CLI_BUILD_URL}) are therefore omitted; users who + * need them can pass {@code --build-name} / {@code --build-number} flags in their command. + */ + private Map createDeploymentEnvironmentVariables( + DeploymentTaskContext taskContext, ServerConfig serverConfig) throws IOException { + + Map envs = new HashMap<>(); + envs.put("JFROG_CLI_SERVER_ID", serverConfig.getServerId()); + + // Isolate the CLI home dir per deployment run using the unique deployment result ID + String deploymentKey = String.valueOf(taskContext.getDeploymentContext().getDeploymentResultId()); + envs.put("JFROG_CLI_HOME_DIR", BambooUtils.getJfrogTmpSubdir(customVariableContext, deploymentKey)); + + // Shared cache for CLI binary and build-info extractors across all deployment runs on this agent + envs.put("JFROG_CLI_DEPENDENCIES_DIR", BambooUtils.getJfrogTmpSubdir(customVariableContext, "dependencies")); + + if (StringUtils.isNotBlank(serverConfig.getCliRepository())) { + envs.put("JFROG_CLI_RELEASES_REPO", serverConfig.getServerId() + "/" + serverConfig.getCliRepository()); + } + + envs.put("JFROG_CLI_USER_AGENT", BambooUtils.getJFrogPluginIdentifier(pluginAccessor)); + envs.put("JFROG_CLI_LOG_TIMESTAMP", "OFF"); + + buildLog.info("The following JFrog CLI environment variables will be used: " + envs); + return envs; + } + + /** + * Runs {@code jf config add} to register a server in the CLI's temp home directory. + */ + private int runConfigAdd(ExecutableRunner commandRunner, ServerConfig serverConfig) + throws IOException, InterruptedException { + List args = new ArrayList<>(List.of( + "config", "add", serverConfig.getServerId(), + "--url=" + serverConfig.getUrl(), + "--interactive=false", + "--overwrite=true" + )); + if (StringUtils.isNotBlank(serverConfig.getAccessToken())) { + args.add("--access-token=" + serverConfig.getAccessToken()); + } else if (StringUtils.isNotBlank(serverConfig.getUsername()) && StringUtils.isNotBlank(serverConfig.getPassword())) { + args.add("--user=" + serverConfig.getUsername()); + args.add("--password=" + serverConfig.getPassword()); + } + return commandRunner.run(args); + } + + /** + * Resolves the working directory, falling back to the task's default if no custom path is set. + */ + private File getWorkingDirectory(String customWd, File defaultWd) throws IOException { + if (StringUtils.isBlank(customWd)) { + return defaultWd; + } + if (!Files.exists(Paths.get(customWd))) { + throw new IOException("Working directory: '" + customWd + "' does not exist."); + } + return new File(customWd); + } + + // Setters for Spring injection and unit-test overrides + + @SuppressWarnings("unused") + public void setServerConfigManager(ServerConfigManager serverConfigManager) { + this.serverConfigManager = serverConfigManager; + } + + @SuppressWarnings("unused") + public void setCustomVariableContext(CustomVariableContext customVariableContext) { + this.customVariableContext = customVariableContext; + } + + @SuppressWarnings("unused") + public void setPluginAccessor(PluginAccessor pluginAccessor) { + this.pluginAccessor = pluginAccessor; + } + + @SuppressWarnings("unused") + public void setAdministrationConfigurationAccessor( + AdministrationConfigurationAccessor administrationConfigurationAccessor) { + this.administrationConfigurationAccessor = administrationConfigurationAccessor; + } +} diff --git a/src/main/resources/atlassian-plugin.xml b/src/main/resources/atlassian-plugin.xml index 3aacc74..086b2cf 100644 --- a/src/main/resources/atlassian-plugin.xml +++ b/src/main/resources/atlassian-plugin.xml @@ -17,6 +17,19 @@ + + + + + + Run JFrog CLI commands from a Bamboo Deployment Project task. + + + Date: Tue, 26 May 2026 21:49:24 +0530 Subject: [PATCH 2/2] RTECO-858: Address PR review comments MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Move shared getWorkingDirectory and runConfigAdd helpers into JfContext so JfDeploymentTask can inherit them instead of duplicating bodies. - Drop the duplicated helpers from JfDeploymentTask. - Remove the unused AdministrationConfigurationAccessor injection (and its setter/import) from JfDeploymentTask — deployment context has no build URL to construct. - Point the JfDeploymentTask in atlassian-plugin.xml at JfContext, since the configurator/UI lives there. Co-authored-by: Cursor --- src/main/java/org/jfrog/bamboo/JfContext.java | 50 +++++++++++++++++++ .../org/jfrog/bamboo/JfDeploymentTask.java | 47 ----------------- src/main/resources/atlassian-plugin.xml | 2 +- 3 files changed, 51 insertions(+), 48 deletions(-) diff --git a/src/main/java/org/jfrog/bamboo/JfContext.java b/src/main/java/org/jfrog/bamboo/JfContext.java index 36c4f99..fdd37e0 100644 --- a/src/main/java/org/jfrog/bamboo/JfContext.java +++ b/src/main/java/org/jfrog/bamboo/JfContext.java @@ -7,9 +7,17 @@ import org.apache.commons.lang.StringUtils; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; +import org.jfrog.bamboo.config.ServerConfig; import org.jfrog.bamboo.config.ServerConfigManager; +import org.jfrog.bamboo.utils.ExecutableRunner; import javax.inject.Inject; +import java.io.File; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Paths; +import java.util.ArrayList; +import java.util.List; import java.util.Map; /** @@ -65,4 +73,46 @@ public void validate(@NotNull ActionParametersMap params, @NotNull ErrorCollecti errorCollection.addErrorMessage("JFrog configuration should be selected"); } } + + /** + * Resolves the task working directory: returns the user-supplied custom path if non-blank + * (failing if it does not exist), otherwise the task's default working directory. + * + *

Lives on the shared configurator so deployment-project subclasses such as + * {@link JfDeploymentTask} can reuse it without re-implementing the same logic. + */ + public File getWorkingDirectory(String customWd, File defaultWd) throws IOException { + if (StringUtils.isBlank(customWd)) { + return defaultWd; + } + if (!Files.exists(Paths.get(customWd))) { + throw new IOException("Working directory: '" + customWd + "' does not exist."); + } + return new File(customWd); + } + + /** + * Runs {@code jf config add} to register a single JFrog server in the CLI's temp home dir. + * + *

Lives on the shared configurator so deployment-project subclasses such as + * {@link JfDeploymentTask} can reuse it without re-implementing the same CLI invocation. + */ + public int runConfigAdd(ExecutableRunner commandRunner, ServerConfig serverConfig) + throws IOException, InterruptedException { + List args = new ArrayList<>(List.of( + "config", + "add", + serverConfig.getServerId(), + "--url=" + serverConfig.getUrl(), + "--interactive=false", + "--overwrite=true" + )); + if (StringUtils.isNotBlank(serverConfig.getAccessToken())) { + args.add("--access-token=" + serverConfig.getAccessToken()); + } else if (StringUtils.isNotBlank(serverConfig.getUsername()) && StringUtils.isNotBlank(serverConfig.getPassword())) { + args.add("--user=" + serverConfig.getUsername()); + args.add("--password=" + serverConfig.getPassword()); + } + return commandRunner.run(args); + } } diff --git a/src/main/java/org/jfrog/bamboo/JfDeploymentTask.java b/src/main/java/org/jfrog/bamboo/JfDeploymentTask.java index 99ec8d7..5dc942a 100644 --- a/src/main/java/org/jfrog/bamboo/JfDeploymentTask.java +++ b/src/main/java/org/jfrog/bamboo/JfDeploymentTask.java @@ -1,6 +1,5 @@ package org.jfrog.bamboo; -import com.atlassian.bamboo.configuration.AdministrationConfigurationAccessor; import com.atlassian.bamboo.deployments.execution.DeploymentTaskContext; import com.atlassian.bamboo.deployments.execution.DeploymentTaskType; import com.atlassian.bamboo.task.TaskResult; @@ -21,9 +20,6 @@ import javax.inject.Inject; import java.io.File; import java.io.IOException; -import java.nio.file.Files; -import java.nio.file.Paths; -import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -54,10 +50,6 @@ public class JfDeploymentTask extends JfContext implements DeploymentTaskType { @ComponentImport private PluginAccessor pluginAccessor; - @Inject - @ComponentImport - private AdministrationConfigurationAccessor administrationConfigurationAccessor; - /** * Executes the JFrog CLI command in a Deployment Project environment. */ @@ -154,39 +146,6 @@ private Map createDeploymentEnvironmentVariables( return envs; } - /** - * Runs {@code jf config add} to register a server in the CLI's temp home directory. - */ - private int runConfigAdd(ExecutableRunner commandRunner, ServerConfig serverConfig) - throws IOException, InterruptedException { - List args = new ArrayList<>(List.of( - "config", "add", serverConfig.getServerId(), - "--url=" + serverConfig.getUrl(), - "--interactive=false", - "--overwrite=true" - )); - if (StringUtils.isNotBlank(serverConfig.getAccessToken())) { - args.add("--access-token=" + serverConfig.getAccessToken()); - } else if (StringUtils.isNotBlank(serverConfig.getUsername()) && StringUtils.isNotBlank(serverConfig.getPassword())) { - args.add("--user=" + serverConfig.getUsername()); - args.add("--password=" + serverConfig.getPassword()); - } - return commandRunner.run(args); - } - - /** - * Resolves the working directory, falling back to the task's default if no custom path is set. - */ - private File getWorkingDirectory(String customWd, File defaultWd) throws IOException { - if (StringUtils.isBlank(customWd)) { - return defaultWd; - } - if (!Files.exists(Paths.get(customWd))) { - throw new IOException("Working directory: '" + customWd + "' does not exist."); - } - return new File(customWd); - } - // Setters for Spring injection and unit-test overrides @SuppressWarnings("unused") @@ -203,10 +162,4 @@ public void setCustomVariableContext(CustomVariableContext customVariableContext public void setPluginAccessor(PluginAccessor pluginAccessor) { this.pluginAccessor = pluginAccessor; } - - @SuppressWarnings("unused") - public void setAdministrationConfigurationAccessor( - AdministrationConfigurationAccessor administrationConfigurationAccessor) { - this.administrationConfigurationAccessor = administrationConfigurationAccessor; - } } diff --git a/src/main/resources/atlassian-plugin.xml b/src/main/resources/atlassian-plugin.xml index 086b2cf..b7c434f 100644 --- a/src/main/resources/atlassian-plugin.xml +++ b/src/main/resources/atlassian-plugin.xml @@ -24,7 +24,7 @@ template are shared with JfTask via JfContext. --> - + Run JFrog CLI commands from a Bamboo Deployment Project task.