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
new file mode 100644
index 0000000..5dc942a
--- /dev/null
+++ b/src/main/java/org/jfrog/bamboo/JfDeploymentTask.java
@@ -0,0 +1,165 @@
+package org.jfrog.bamboo;
+
+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.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;
+
+ /**
+ * 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;
+ }
+
+ // 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;
+ }
+}
diff --git a/src/main/resources/atlassian-plugin.xml b/src/main/resources/atlassian-plugin.xml
index 3aacc74..b7c434f 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.
+
+
+