diff --git a/api/src/main/java/org/jfrog/artifactory/client/Builds.java b/api/src/main/java/org/jfrog/artifactory/client/Builds.java index e4b32751..e32d2094 100644 --- a/api/src/main/java/org/jfrog/artifactory/client/Builds.java +++ b/api/src/main/java/org/jfrog/artifactory/client/Builds.java @@ -1,7 +1,10 @@ package org.jfrog.artifactory.client; import org.jfrog.artifactory.client.model.AllBuilds; +import org.jfrog.artifactory.client.model.BuildPromotionRequest; +import org.jfrog.artifactory.client.model.BuildPromotionResponse; import org.jfrog.artifactory.client.model.BuildRuns; +import org.jfrog.build.api.Build; import java.io.IOException; @@ -12,4 +15,44 @@ public interface Builds { AllBuilds getAllBuilds() throws IOException; BuildRuns getBuildRuns(String buildName) throws IOException; + + /** + * Upload a build to Artifactory using the official build-info API + * + * @param build the build info from org.jfrog.build.api.Build + * @throws IOException if the upload fails + */ + void uploadBuild(Build build) throws IOException; + + /** + * Upload a build to Artifactory with a project parameter using the official build-info API + * + * @param build the build info from org.jfrog.build.api.Build + * @param project the project name to limit the build to + * @throws IOException if the upload fails + */ + void uploadBuild(Build build, String project) throws IOException; + + /** + * Promote a build in Artifactory + * + * @param buildName the name of the build to promote + * @param buildNumber the number of the build to promote + * @param promotionRequest the promotion request details + * @return the promotion response with messages + * @throws IOException if the promotion fails + */ + BuildPromotionResponse promoteBuild(String buildName, String buildNumber, BuildPromotionRequest promotionRequest) throws IOException; + + /** + * Promote a build in Artifactory with a project parameter + * + * @param buildName the name of the build to promote + * @param buildNumber the number of the build to promote + * @param promotionRequest the promotion request details + * @param project the project name + * @return the promotion response with messages + * @throws IOException if the promotion fails + */ + BuildPromotionResponse promoteBuild(String buildName, String buildNumber, BuildPromotionRequest promotionRequest, String project) throws IOException; } diff --git a/api/src/main/java/org/jfrog/artifactory/client/model/BuildPromotionRequest.java b/api/src/main/java/org/jfrog/artifactory/client/model/BuildPromotionRequest.java new file mode 100644 index 00000000..3bb09323 --- /dev/null +++ b/api/src/main/java/org/jfrog/artifactory/client/model/BuildPromotionRequest.java @@ -0,0 +1,100 @@ +package org.jfrog.artifactory.client.model; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonProperty; + +import java.util.List; +import java.util.Map; + +/** + * Request for promoting a build in Artifactory + * + * @author rnc + */ +@JsonIgnoreProperties(ignoreUnknown = true) +public interface BuildPromotionRequest { + /** + * The new status of the build + * @return the status + */ + String getStatus(); + + /** + * An optional comment describing the reason for the promotion + * @return the comment + */ + String getComment(); + + /** + * The user that invoked promotion from the CI server + * @return the CI user + */ + @JsonProperty("ciUser") + String getCiUser(); + + /** + * The time when the promotion command was received by Artifactory (ISO8601 format) + * @return the timestamp + */ + String getTimestamp(); + + /** + * When set to true, performs a dry run of the promotion without executing any operation + * @return true for dry run + */ + @JsonProperty("dryRun") + Boolean getDryRun(); + + /** + * The repository from which the build contents will be copied or moved + * @return the source repository + */ + @JsonProperty("sourceRepo") + String getSourceRepo(); + + /** + * The target repository to which the build contents will be copied or moved + * @return the target repository + */ + @JsonProperty("targetRepo") + String getTargetRepo(); + + /** + * Determines how to perform the build promotion. true = copy, false = move + * @return true to copy, false to move + */ + Boolean getCopy(); + + /** + * Determines whether to move/copy the build's artifacts + * @return true to include artifacts + */ + Boolean getArtifacts(); + + /** + * Determines whether to move/copy the build's dependencies + * @return true to include dependencies + */ + Boolean getDependencies(); + + /** + * An array of dependency scopes + * @return the scopes + */ + List getScopes(); + + /** + * A list of properties to attach to the build's artifacts + * @return the properties + */ + Map getProperties(); + + /** + * When set to true, fails and aborts the promotion operation upon receiving an error + * @return true to fail fast + */ + @JsonProperty("failFast") + Boolean getFailFast(); +} + +// Made with Bob diff --git a/api/src/main/java/org/jfrog/artifactory/client/model/BuildPromotionResponse.java b/api/src/main/java/org/jfrog/artifactory/client/model/BuildPromotionResponse.java new file mode 100644 index 00000000..80891f61 --- /dev/null +++ b/api/src/main/java/org/jfrog/artifactory/client/model/BuildPromotionResponse.java @@ -0,0 +1,21 @@ +package org.jfrog.artifactory.client.model; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; + +import java.util.List; + +/** + * Response from promoting a build in Artifactory + * + * @author rnc + */ +@JsonIgnoreProperties(ignoreUnknown = true) +public interface BuildPromotionResponse { + /** + * Get the list of messages from the promotion operation + * @return the messages + */ + List getMessages(); +} + +// Made with Bob diff --git a/api/src/main/java/org/jfrog/artifactory/client/model/PromotionMessage.java b/api/src/main/java/org/jfrog/artifactory/client/model/PromotionMessage.java new file mode 100644 index 00000000..c3dc5ff2 --- /dev/null +++ b/api/src/main/java/org/jfrog/artifactory/client/model/PromotionMessage.java @@ -0,0 +1,25 @@ +package org.jfrog.artifactory.client.model; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; + +/** + * A message returned from a build promotion operation + * + * @author rnc + */ +@JsonIgnoreProperties(ignoreUnknown = true) +public interface PromotionMessage { + /** + * The level of the message (error, warning, info) + * @return the message level + */ + String getLevel(); + + /** + * The message text + * @return the message + */ + String getMessage(); +} + +// Made with Bob diff --git a/build.gradle b/build.gradle index a3f852f1..80db9e4d 100644 --- a/build.gradle +++ b/build.gradle @@ -86,6 +86,7 @@ subprojects { implementation 'com.fasterxml.jackson.core:jackson-databind:2.21.1' implementation 'com.fasterxml.jackson.core:jackson-annotations:2.21' api 'org.jfrog.filespecs:file-specs-java:1.1.2' + api 'org.jfrog.buildinfo:build-info-api:2.+' } task sourcesJar(type: Jar, dependsOn: classes) { diff --git a/services/src/main/java/org/jfrog/artifactory/client/impl/BuildsImpl.java b/services/src/main/java/org/jfrog/artifactory/client/impl/BuildsImpl.java index a6500d92..0f9d0447 100644 --- a/services/src/main/java/org/jfrog/artifactory/client/impl/BuildsImpl.java +++ b/services/src/main/java/org/jfrog/artifactory/client/impl/BuildsImpl.java @@ -1,13 +1,21 @@ package org.jfrog.artifactory.client.impl; +import org.apache.http.entity.ContentType; import org.jfrog.artifactory.client.Artifactory; import org.jfrog.artifactory.client.Builds; +import org.jfrog.artifactory.client.impl.util.Util; import org.jfrog.artifactory.client.model.AllBuilds; +import org.jfrog.artifactory.client.model.BuildPromotionRequest; +import org.jfrog.artifactory.client.model.BuildPromotionResponse; import org.jfrog.artifactory.client.model.BuildRuns; import org.jfrog.artifactory.client.model.impl.AllBuildsImpl; +import org.jfrog.artifactory.client.model.impl.BuildPromotionResponseImpl; import org.jfrog.artifactory.client.model.impl.BuildRunsImpl; +import org.jfrog.build.api.Build; import java.io.IOException; +import java.util.HashMap; +import java.util.Map; /** * @author yahavi @@ -31,6 +39,42 @@ public BuildRuns getBuildRuns(String buildName) throws IOException { return artifactory.get(getBuilderApi() + buildName, BuildRunsImpl.class, BuildRuns.class); } + @Override + public void uploadBuild(Build build) throws IOException { + uploadBuild(build, null); + } + + @Override + public void uploadBuild(Build build, String project) throws IOException { + String apiPath = getBuilderApi(); + if (project != null && !project.isEmpty()) { + apiPath += "?project=" + project; + } + + Map headers = new HashMap<>(); + artifactory.put(apiPath, ContentType.APPLICATION_JSON, + Util.getStringFromObject(build), headers, null, -1, + String.class, null); + } + + @Override + public BuildPromotionResponse promoteBuild(String buildName, String buildNumber, BuildPromotionRequest promotionRequest) throws IOException { + return promoteBuild(buildName, buildNumber, promotionRequest, null); + } + + @Override + public BuildPromotionResponse promoteBuild(String buildName, String buildNumber, BuildPromotionRequest promotionRequest, String project) throws IOException { + String apiPath = getBuilderApi() + "promote/" + buildName + "/" + buildNumber; + if (project != null && !project.isEmpty()) { + apiPath += "?project=" + project; + } + + Map headers = new HashMap<>(); + return artifactory.post(apiPath, ContentType.APPLICATION_JSON, + Util.getStringFromObject(promotionRequest), headers, + BuildPromotionResponseImpl.class, BuildPromotionResponse.class); + } + public String getBuilderApi() { return baseApiPath + "/build/"; } diff --git a/services/src/main/java/org/jfrog/artifactory/client/model/impl/BuildPromotionRequestImpl.java b/services/src/main/java/org/jfrog/artifactory/client/model/impl/BuildPromotionRequestImpl.java new file mode 100644 index 00000000..1e583c01 --- /dev/null +++ b/services/src/main/java/org/jfrog/artifactory/client/model/impl/BuildPromotionRequestImpl.java @@ -0,0 +1,152 @@ +package org.jfrog.artifactory.client.model.impl; + +import com.fasterxml.jackson.annotation.JsonProperty; +import org.jfrog.artifactory.client.model.BuildPromotionRequest; + +import java.util.List; +import java.util.Map; + +/** + * Implementation of BuildPromotionRequest + * + * @author rnc + */ +public class BuildPromotionRequestImpl implements BuildPromotionRequest { + private String status; + private String comment; + @JsonProperty("ciUser") + private String ciUser; + private String timestamp; + @JsonProperty("dryRun") + private Boolean dryRun; + @JsonProperty("sourceRepo") + private String sourceRepo; + @JsonProperty("targetRepo") + private String targetRepo; + private Boolean copy; + private Boolean artifacts; + private Boolean dependencies; + private List scopes; + private Map properties; + @JsonProperty("failFast") + private Boolean failFast; + + @Override + public String getStatus() { + return status; + } + + public void setStatus(String status) { + this.status = status; + } + + @Override + public String getComment() { + return comment; + } + + public void setComment(String comment) { + this.comment = comment; + } + + @Override + public String getCiUser() { + return ciUser; + } + + public void setCiUser(String ciUser) { + this.ciUser = ciUser; + } + + @Override + public String getTimestamp() { + return timestamp; + } + + public void setTimestamp(String timestamp) { + this.timestamp = timestamp; + } + + @Override + public Boolean getDryRun() { + return dryRun; + } + + public void setDryRun(Boolean dryRun) { + this.dryRun = dryRun; + } + + @Override + public String getSourceRepo() { + return sourceRepo; + } + + public void setSourceRepo(String sourceRepo) { + this.sourceRepo = sourceRepo; + } + + @Override + public String getTargetRepo() { + return targetRepo; + } + + public void setTargetRepo(String targetRepo) { + this.targetRepo = targetRepo; + } + + @Override + public Boolean getCopy() { + return copy; + } + + public void setCopy(Boolean copy) { + this.copy = copy; + } + + @Override + public Boolean getArtifacts() { + return artifacts; + } + + public void setArtifacts(Boolean artifacts) { + this.artifacts = artifacts; + } + + @Override + public Boolean getDependencies() { + return dependencies; + } + + public void setDependencies(Boolean dependencies) { + this.dependencies = dependencies; + } + + @Override + public List getScopes() { + return scopes; + } + + public void setScopes(List scopes) { + this.scopes = scopes; + } + + @Override + public Map getProperties() { + return properties; + } + + public void setProperties(Map properties) { + this.properties = properties; + } + + @Override + public Boolean getFailFast() { + return failFast; + } + + public void setFailFast(Boolean failFast) { + this.failFast = failFast; + } +} + +// Made with Bob diff --git a/services/src/main/java/org/jfrog/artifactory/client/model/impl/BuildPromotionResponseImpl.java b/services/src/main/java/org/jfrog/artifactory/client/model/impl/BuildPromotionResponseImpl.java new file mode 100644 index 00000000..36017c87 --- /dev/null +++ b/services/src/main/java/org/jfrog/artifactory/client/model/impl/BuildPromotionResponseImpl.java @@ -0,0 +1,26 @@ +package org.jfrog.artifactory.client.model.impl; + +import org.jfrog.artifactory.client.model.BuildPromotionResponse; +import org.jfrog.artifactory.client.model.PromotionMessage; + +import java.util.List; + +/** + * Implementation of BuildPromotionResponse + * + * @author rnc + */ +public class BuildPromotionResponseImpl implements BuildPromotionResponse { + private List messages; + + @Override + public List getMessages() { + return messages; + } + + public void setMessages(List messages) { + this.messages = messages; + } +} + +// Made with Bob diff --git a/services/src/main/java/org/jfrog/artifactory/client/model/impl/PromotionMessageImpl.java b/services/src/main/java/org/jfrog/artifactory/client/model/impl/PromotionMessageImpl.java new file mode 100644 index 00000000..b5e3b3a0 --- /dev/null +++ b/services/src/main/java/org/jfrog/artifactory/client/model/impl/PromotionMessageImpl.java @@ -0,0 +1,33 @@ +package org.jfrog.artifactory.client.model.impl; + +import org.jfrog.artifactory.client.model.PromotionMessage; + +/** + * Implementation of PromotionMessage + * + * @author rnc + */ +public class PromotionMessageImpl implements PromotionMessage { + private String level; + private String message; + + @Override + public String getLevel() { + return level; + } + + public void setLevel(String level) { + this.level = level; + } + + @Override + public String getMessage() { + return message; + } + + public void setMessage(String message) { + this.message = message; + } +} + +// Made with Bob diff --git a/services/src/test/java/org/jfrog/artifactory/client/BuildsTests.java b/services/src/test/java/org/jfrog/artifactory/client/BuildsTests.java index c74668a6..4799ec48 100644 --- a/services/src/test/java/org/jfrog/artifactory/client/BuildsTests.java +++ b/services/src/test/java/org/jfrog/artifactory/client/BuildsTests.java @@ -2,20 +2,24 @@ import org.apache.commons.lang3.StringUtils; import org.jfrog.artifactory.client.model.AllBuilds; -import org.jfrog.artifactory.client.model.Build; import org.jfrog.artifactory.client.model.BuildNumber; +import org.jfrog.artifactory.client.model.BuildPromotionResponse; import org.jfrog.artifactory.client.model.BuildRuns; +import org.jfrog.artifactory.client.model.PromotionMessage; +import org.jfrog.artifactory.client.model.impl.BuildPromotionRequestImpl; +import org.jfrog.build.api.Build; import org.testng.annotations.BeforeClass; import org.testng.annotations.Test; import java.io.IOException; +import java.text.SimpleDateFormat; import java.util.List; import java.util.Map; +import static org.jfrog.artifactory.client.Utils.createBuild; import static org.jfrog.artifactory.client.Utils.createBuildBody; import static org.jfrog.artifactory.client.Utils.uploadBuild; -import static org.testng.Assert.assertNotNull; -import static org.testng.Assert.assertTrue; +import static org.testng.Assert.*; /** * @author yahavi @@ -23,6 +27,13 @@ public class BuildsTests extends ArtifactoryTestsBase { private static final String BUILDS_API = "/api/build"; + private static final String TEST_BUILD_NAME = "TestBuild"; + private static final String TEST_BUILD_NUMBER = "13"; + private static final String UPLOAD_TEST_BUILD_NAME = "UploadTestBuild"; + private static final String UPLOAD_TEST_BUILD_NUMBER = "100"; + private static final String PROMOTE_TEST_BUILD_NAME = "PromoteTestBuild"; + private static final String PROMOTE_TEST_BUILD_NUMBER = "200"; + private Map buildBody; @BeforeClass @@ -38,12 +49,12 @@ public void testGetAllBuilds() throws Exception { assertNotNull(allBuilds); assertTrue(StringUtils.contains(allBuilds.getUri(), BUILDS_API), allBuilds.getUri() + " is expected to contains '" + BUILDS_API + "'"); - List actualBuilds = allBuilds.getBuilds(); + List actualBuilds = allBuilds.getBuilds(); assertNotNull(actualBuilds); // Assert build uri "/TestBuild" exist String expectedBuildUri = "/" + getExpectedBuildName(); - Build actualBuild = actualBuilds.stream() + org.jfrog.artifactory.client.model.Build actualBuild = actualBuilds.stream() .filter(build -> StringUtils.equals(build.getUri(), expectedBuildUri)) .findAny().orElse(null); assertNotNull(actualBuild, "Build Uri " + expectedBuildUri + " does not exist in [" + actualBuilds + "]"); @@ -67,6 +78,69 @@ public void testGetBuildRuns() throws IOException { assertTrue(StringUtils.isNotBlank(buildNumber.getStarted())); } + @Test + public void testUploadBuild() throws IOException { + // Create a new build using the build-info API + Build build = createBuild(); + + // Modify the build name and number to avoid conflicts + build.setName(UPLOAD_TEST_BUILD_NAME); + build.setNumber(UPLOAD_TEST_BUILD_NUMBER); + + // Upload the build + artifactory.builds().uploadBuild(build); + + // Verify the build was uploaded by retrieving it + BuildRuns buildRuns = artifactory.builds().getBuildRuns(UPLOAD_TEST_BUILD_NAME); + assertNotNull(buildRuns); + + // Check that our build number exists + BuildNumber buildNumber = buildRuns.getBuildsNumbers().stream() + .filter(bn -> StringUtils.equals(bn.getUri(), "/" + UPLOAD_TEST_BUILD_NUMBER)) + .findAny().orElse(null); + assertNotNull(buildNumber, "Build number " + UPLOAD_TEST_BUILD_NUMBER + " was not found after upload"); + } + + @Test + public void testPromoteBuild() throws IOException { + // First upload a build to promote using the build-info API + Build build = createBuild(); + build.setName(PROMOTE_TEST_BUILD_NAME); + build.setNumber(PROMOTE_TEST_BUILD_NUMBER); + artifactory.builds().uploadBuild(build); + + // Create promotion request + BuildPromotionRequestImpl promotionRequest = new BuildPromotionRequestImpl(); + promotionRequest.setStatus("Released"); + promotionRequest.setComment("Promoted by automated test"); + promotionRequest.setCiUser("testUser"); + promotionRequest.setTimestamp(new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSSZ").format(System.currentTimeMillis())); + promotionRequest.setCopy(false); + promotionRequest.setArtifacts(true); + promotionRequest.setDependencies(false); + promotionRequest.setFailFast(true); + promotionRequest.setDryRun(true); // Use dry run to avoid needing actual artifacts + + // Promote the build + BuildPromotionResponse response = artifactory.builds().promoteBuild( + PROMOTE_TEST_BUILD_NAME, + PROMOTE_TEST_BUILD_NUMBER, + promotionRequest + ); + + // Verify response + assertNotNull(response); + assertNotNull(response.getMessages()); + + // In dry run mode, we should get messages about what would happen + if (!response.getMessages().isEmpty()) { + for (PromotionMessage message : response.getMessages()) { + assertNotNull(message.getLevel()); + assertNotNull(message.getMessage()); + } + } + } + private String getExpectedBuildName() { return (String) buildBody.get("name"); } diff --git a/services/src/test/java/org/jfrog/artifactory/client/Utils.java b/services/src/test/java/org/jfrog/artifactory/client/Utils.java index 5db481b1..b565871a 100644 --- a/services/src/test/java/org/jfrog/artifactory/client/Utils.java +++ b/services/src/test/java/org/jfrog/artifactory/client/Utils.java @@ -5,6 +5,7 @@ import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.exception.ExceptionUtils; import org.jfrog.artifactory.client.impl.ArtifactoryRequestImpl; +import org.jfrog.build.api.Build; import java.io.IOException; import java.nio.charset.StandardCharsets; @@ -40,4 +41,17 @@ public static Map createBuildBody() { } return new HashMap<>(); } + + public static Build createBuild() { + String buildStarted = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSSZ").format(System.currentTimeMillis()); + try { + String buildInfoJson = IOUtils.toString(Utils.class.getResourceAsStream("/build.json"), StandardCharsets.UTF_8); + buildInfoJson = StringUtils.replace(buildInfoJson, "{build.start.time}", buildStarted); + ObjectMapper mapper = new ObjectMapper(); + return mapper.readValue(buildInfoJson, Build.class); + } catch (IOException e) { + fail(ExceptionUtils.getRootCauseMessage(e)); + } + return null; + } }