You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@sling.apache.org by ra...@apache.org on 2019/12/16 10:28:19 UTC

[sling-org-apache-sling-committer-cli] branch issue/SLING-8392 updated (b757503 -> babbd05)

This is an automated email from the ASF dual-hosted git repository.

radu pushed a change to branch issue/SLING-8392
in repository https://gitbox.apache.org/repos/asf/sling-org-apache-sling-committer-cli.git.


 discard b757503  SLING-8392 - Create sub-command to manage the Jira update when promoting a release
 discard ef11655  SLING-8392 - Create sub-command to manage the Jira update when promoting a release
     add 1569b7c  tests minor code cleanup
     new babbd05  SLING-8392 - Create sub-command to manage the Jira update when promoting a release

This update added new revisions after undoing existing revisions.
That is to say, some revisions that were in the old version of the
branch are not in the new version.  This situation occurs
when a user --force pushes a change and generates a repository
containing something like this:

 * -- * -- B -- O -- O -- O   (b757503)
            \
             N -- N -- N   refs/heads/issue/SLING-8392 (babbd05)

You should already have received notification emails for all of the O
revisions, and so the following emails describe only the N revisions
from the common base, B.

Any revisions marked "omit" are not gone; other references still
refer to them.  Any revisions marked "discard" are gone forever.

The 1 revisions listed above as "new" are entirely new to this
repository and will be described in separate emails.  The revisions
listed as "add" were already present in the repository and have only
been added to this reference.


Summary of changes:
 .../org/apache/sling/cli/impl/jira/Version.java    |  1 +
 .../apache/sling/cli/impl/jira/VersionClient.java  |  6 +-----
 .../cli/impl/jbake/JBakeContentUpdaterTest.java    | 20 ++++++++++--------
 .../cli/impl/jira/CreateVersionJiraAction.java     |  8 +++++---
 .../sling/cli/impl/jira/EditVersionJiraAction.java |  4 +++-
 .../sling/cli/impl/jira/TransitionsJiraAction.java |  2 +-
 .../sling/cli/impl/jira/VersionClientTest.java     | 24 +---------------------
 .../sling/cli/impl/junit/SystemPropertiesRule.java |  6 +++---
 .../cli/impl/pgp/PGPSignatureValidatorTest.java    | 11 +++++-----
 .../impl/release/UpdateReporterCommandTest.java    |  4 +---
 10 files changed, 32 insertions(+), 54 deletions(-)


[sling-org-apache-sling-committer-cli] 01/01: SLING-8392 - Create sub-command to manage the Jira update when promoting a release

Posted by ra...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

radu pushed a commit to branch issue/SLING-8392
in repository https://gitbox.apache.org/repos/asf/sling-org-apache-sling-committer-cli.git

commit babbd055bae61473b7298fecb2fb72b96d3bd8bd
Author: Radu Cotescu <ra...@apache.org>
AuthorDate: Fri Dec 13 18:46:04 2019 +0100

    SLING-8392 - Create sub-command to manage the Jira update when promoting a release
---
 pom.xml                                            |   6 +
 src/main/features/app.json                         |   4 +
 .../org/apache/sling/cli/impl/DateProvider.java    |   8 +-
 .../java/org/apache/sling/cli/impl/jira/Issue.java |  26 +++-
 .../{DateProvider.java => jira/Transition.java}    |  27 ++--
 .../{DateProvider.java => jira/Transitions.java}   |  22 ++-
 .../org/apache/sling/cli/impl/jira/Version.java    |  21 ++-
 .../apache/sling/cli/impl/jira/VersionClient.java  | 159 ++++++++++++++++++---
 .../impl/release/ReleaseJiraVersionCommand.java    | 105 ++++++++++++++
 .../sling/cli/impl/http/HttpExchangeHandler.java   |  19 +--
 .../sling/cli/impl/jira/EditVersionJiraAction.java |  98 +++++++++++++
 .../cli/impl/jira/IssuesSearchJiraAction.java      |  18 ++-
 .../org/apache/sling/cli/impl/jira/MockJira.java   |  16 ++-
 .../sling/cli/impl/jira/TransitionsJiraAction.java |  66 +++++++++
 .../sling/cli/impl/jira/VersionClientTest.java     |  51 +++++--
 src/test/resources/jira/relatedIssueCounts/0.json  |   6 +
 ...ter-cli-1.0.0.json => committer-cli-1.0.0.json} |  84 +++++++++--
 .../resources/jira/search/transitions-1.0.0.json   |  53 +++++++
 .../resources/jira/search/transitions-2.0.0.json   |  53 +++++++
 .../search/unresolved-committer-cli-1.0.0.json     |  26 ----
 .../resources/jira/transitions/no-transitions.json |   5 +
 .../resources/jira/transitions/transitions.json    |  41 ++++++
 src/test/resources/jira/versions.json              |  11 ++
 23 files changed, 815 insertions(+), 110 deletions(-)

diff --git a/pom.xml b/pom.xml
index 4aac3cb..59339ec 100644
--- a/pom.xml
+++ b/pom.xml
@@ -200,6 +200,12 @@
         </dependency>
         <dependency>
             <groupId>org.osgi</groupId>
+            <artifactId>osgi.promise</artifactId>
+            <version>7.0.1</version>
+            <scope>provided</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.osgi</groupId>
             <artifactId>osgi.core</artifactId>
         </dependency>
         <dependency>
diff --git a/src/main/features/app.json b/src/main/features/app.json
index 805b29c..4e6e732 100644
--- a/src/main/features/app.json
+++ b/src/main/features/app.json
@@ -66,6 +66,10 @@
             "start-level": "3"
         },
         {
+            "id"         : "org.osgi:osgi.promise:7.0.1",
+            "start-level": "4"
+        },
+        {
             "id": "org.bouncycastle:bcpg-jdk15on:1.62"
         },
         {
diff --git a/src/main/java/org/apache/sling/cli/impl/DateProvider.java b/src/main/java/org/apache/sling/cli/impl/DateProvider.java
index 627cca8..9e114d1 100644
--- a/src/main/java/org/apache/sling/cli/impl/DateProvider.java
+++ b/src/main/java/org/apache/sling/cli/impl/DateProvider.java
@@ -27,14 +27,20 @@ import org.osgi.service.component.annotations.Component;
 public class DateProvider {
 
     private static final DateTimeFormatter emailDateHeader = DateTimeFormatter.ofPattern("EEE, d MMM yyyy HH:mm:ss Z");
+    private static final DateTimeFormatter jiraReleaseDate = DateTimeFormatter.ofPattern("yyyy-MM-d");
 
     public OffsetDateTime getCurrentDate() {
         return OffsetDateTime.now();
     }
 
     public String getCurrentDateForEmailHeader() {
-        OffsetDateTime offsetDateTime = OffsetDateTime.now();
+        OffsetDateTime offsetDateTime = getCurrentDate();
         return offsetDateTime.format(emailDateHeader);
     }
 
+    public String getCurrentDateForJiraRelease() {
+        OffsetDateTime offsetDateTime = getCurrentDate();
+        return offsetDateTime.format(jiraReleaseDate);
+    }
+
 }
diff --git a/src/main/java/org/apache/sling/cli/impl/jira/Issue.java b/src/main/java/org/apache/sling/cli/impl/jira/Issue.java
index 1fb3c6c..87c474f 100644
--- a/src/main/java/org/apache/sling/cli/impl/jira/Issue.java
+++ b/src/main/java/org/apache/sling/cli/impl/jira/Issue.java
@@ -33,9 +33,33 @@ public class Issue {
     public String getSummary() {
         return fields.summary;
     }
+
+    public String getStatus() {
+        if (fields.status != null) {
+            return fields.status.name;
+        }
+        return null;
+    }
+
+    public String getResolution() {
+        if (fields.resolution != null) {
+            return fields.resolution.name;
+        }
+        return null;
+    }
     
     static class Fields {
-        private String summary;    
+        private String summary;
+        private Status status;
+        private Resolution resolution;
+
+        static class Status {
+            private String name;
+        }
+
+        static class Resolution {
+            private String name;
+        }
     }
 
     @Override
diff --git a/src/main/java/org/apache/sling/cli/impl/DateProvider.java b/src/main/java/org/apache/sling/cli/impl/jira/Transition.java
similarity index 61%
copy from src/main/java/org/apache/sling/cli/impl/DateProvider.java
copy to src/main/java/org/apache/sling/cli/impl/jira/Transition.java
index 627cca8..e5a10df 100644
--- a/src/main/java/org/apache/sling/cli/impl/DateProvider.java
+++ b/src/main/java/org/apache/sling/cli/impl/jira/Transition.java
@@ -16,25 +16,26 @@
  ~ specific language governing permissions and limitations
  ~ under the License.
  ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~*/
-package org.apache.sling.cli.impl;
+package org.apache.sling.cli.impl.jira;
 
-import java.time.OffsetDateTime;
-import java.time.format.DateTimeFormatter;
+public class Transition {
 
-import org.osgi.service.component.annotations.Component;
+    private int id;
+    private String name;
 
-@Component(service = DateProvider.class)
-public class DateProvider {
-
-    private static final DateTimeFormatter emailDateHeader = DateTimeFormatter.ofPattern("EEE, d MMM yyyy HH:mm:ss Z");
+    public int getId() {
+        return id;
+    }
 
-    public OffsetDateTime getCurrentDate() {
-        return OffsetDateTime.now();
+    public void setId(int id) {
+        this.id = id;
     }
 
-    public String getCurrentDateForEmailHeader() {
-        OffsetDateTime offsetDateTime = OffsetDateTime.now();
-        return offsetDateTime.format(emailDateHeader);
+    public String getName() {
+        return name;
     }
 
+    public void setName(String name) {
+        this.name = name;
+    }
 }
diff --git a/src/main/java/org/apache/sling/cli/impl/DateProvider.java b/src/main/java/org/apache/sling/cli/impl/jira/Transitions.java
similarity index 61%
copy from src/main/java/org/apache/sling/cli/impl/DateProvider.java
copy to src/main/java/org/apache/sling/cli/impl/jira/Transitions.java
index 627cca8..f0da543 100644
--- a/src/main/java/org/apache/sling/cli/impl/DateProvider.java
+++ b/src/main/java/org/apache/sling/cli/impl/jira/Transitions.java
@@ -16,25 +16,19 @@
  ~ specific language governing permissions and limitations
  ~ under the License.
  ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~*/
-package org.apache.sling.cli.impl;
+package org.apache.sling.cli.impl.jira;
 
-import java.time.OffsetDateTime;
-import java.time.format.DateTimeFormatter;
+import java.util.List;
 
-import org.osgi.service.component.annotations.Component;
+public class Transitions {
 
-@Component(service = DateProvider.class)
-public class DateProvider {
+    private List<Transition> transitions;
 
-    private static final DateTimeFormatter emailDateHeader = DateTimeFormatter.ofPattern("EEE, d MMM yyyy HH:mm:ss Z");
-
-    public OffsetDateTime getCurrentDate() {
-        return OffsetDateTime.now();
+    public List<Transition> getTransitions() {
+        return transitions;
     }
 
-    public String getCurrentDateForEmailHeader() {
-        OffsetDateTime offsetDateTime = OffsetDateTime.now();
-        return offsetDateTime.format(emailDateHeader);
+    public void setTransitions(List<Transition> transitions) {
+        this.transitions = transitions;
     }
-
 }
diff --git a/src/main/java/org/apache/sling/cli/impl/jira/Version.java b/src/main/java/org/apache/sling/cli/impl/jira/Version.java
index 173971e..0e9dffb 100644
--- a/src/main/java/org/apache/sling/cli/impl/jira/Version.java
+++ b/src/main/java/org/apache/sling/cli/impl/jira/Version.java
@@ -16,10 +16,13 @@
  */
 package org.apache.sling.cli.impl.jira;
 
+@SuppressWarnings("unused")
 public class Version {
     private int id;
     private String name;
     private int issuesFixedCount;
+    private boolean released;
+    private String releaseDate;
 
     public int getId() {
         return id;
@@ -44,7 +47,23 @@ public class Version {
     public void setRelatedIssuesCount(int relatedIssuesCount) {
         this.issuesFixedCount = relatedIssuesCount;
     }
-    
+
+    public boolean isReleased() {
+        return released;
+    }
+
+    public void setReleased(boolean released) {
+        this.released = released;
+    }
+
+    public String getReleaseDate() {
+        return releaseDate;
+    }
+
+    public void setReleaseDate(String releaseDate) {
+        this.releaseDate = releaseDate;
+    }
+
     @Override
     public String toString() {
         
diff --git a/src/main/java/org/apache/sling/cli/impl/jira/VersionClient.java b/src/main/java/org/apache/sling/cli/impl/jira/VersionClient.java
index 0621199..57e2a0e 100644
--- a/src/main/java/org/apache/sling/cli/impl/jira/VersionClient.java
+++ b/src/main/java/org/apache/sling/cli/impl/jira/VersionClient.java
@@ -23,9 +23,11 @@ import java.io.StringWriter;
 import java.lang.reflect.Type;
 import java.net.URISyntaxException;
 import java.nio.charset.StandardCharsets;
+import java.util.ArrayList;
 import java.util.List;
 import java.util.Optional;
 import java.util.function.Predicate;
+import java.util.stream.Collectors;
 
 import org.apache.http.HttpHeaders;
 import org.apache.http.client.methods.CloseableHttpResponse;
@@ -36,12 +38,16 @@ import org.apache.http.client.utils.URIBuilder;
 import org.apache.http.entity.StringEntity;
 import org.apache.http.impl.client.CloseableHttpClient;
 import org.apache.sling.cli.impl.ComponentContextHelper;
+import org.apache.sling.cli.impl.DateProvider;
 import org.apache.sling.cli.impl.http.HttpClientFactory;
 import org.apache.sling.cli.impl.release.Release;
 import org.osgi.service.component.ComponentContext;
 import org.osgi.service.component.annotations.Activate;
 import org.osgi.service.component.annotations.Component;
 import org.osgi.service.component.annotations.Reference;
+import org.osgi.util.promise.FailedPromisesException;
+import org.osgi.util.promise.Promise;
+import org.osgi.util.promise.PromiseFactory;
 
 import com.google.gson.Gson;
 import com.google.gson.JsonIOException;
@@ -54,19 +60,27 @@ import com.google.gson.stream.JsonWriter;
  */
 @Component(service = VersionClient.class)
 public class VersionClient {
-    
+
     private static final String PROJECT_KEY = "SLING";
-    private static final String DEFAULT_JIRA_URL_PREFIX = "https://issues.apache.org/jira/rest/api/2/";
+    private static final String DEFAULT_JIRA_URL = "https://issues.apache.org/jira";
     private static final String CONTENT_TYPE_JSON = "application/json";
-    
+
+    private final PromiseFactory promiseFactory = new PromiseFactory(null, null);
+
     @Reference
     private HttpClientFactory httpClientFactory;
-    private String jiraUrlPrefix;
+
+    @Reference
+    private DateProvider dateProvider;
+
+    private String jiraRESTAPIEntrypoint;
+    private String jiraURL;
 
     @Activate
     protected void activate(ComponentContext ctx) {
         ComponentContextHelper helper = ComponentContextHelper.wrap(ctx);
-        jiraUrlPrefix = helper.getProperty("jira.url.prefix", DEFAULT_JIRA_URL_PREFIX);
+        jiraURL = helper.getProperty("jira.url", DEFAULT_JIRA_URL);
+        jiraRESTAPIEntrypoint = jiraURL + "/rest/api/2/";
     }
 
     /**
@@ -160,35 +174,100 @@ public class VersionClient {
         }
     }
 
+    private HttpGet newGet(String suffix) {
+        HttpGet get = new HttpGet(jiraRESTAPIEntrypoint + suffix);
+        get.addHeader(HttpHeaders.ACCEPT, CONTENT_TYPE_JSON);
+        return get;
+    }
+
     private HttpPost newPost(String suffix) {
-        HttpPost post = new HttpPost(jiraUrlPrefix + suffix);
+        HttpPost post = new HttpPost(jiraRESTAPIEntrypoint + suffix);
         post.addHeader(HttpHeaders.CONTENT_TYPE, CONTENT_TYPE_JSON);
         post.addHeader(HttpHeaders.ACCEPT, CONTENT_TYPE_JSON);
         return post;
     }
     
     private HttpPut newPut(String suffix) {
-        HttpPut put = new HttpPut(jiraUrlPrefix + suffix);
+        HttpPut put = new HttpPut(jiraRESTAPIEntrypoint + suffix);
         put.addHeader(HttpHeaders.CONTENT_TYPE, CONTENT_TYPE_JSON);
         put.addHeader(HttpHeaders.ACCEPT, CONTENT_TYPE_JSON);
         return put;
     }
     
     public List<Issue> findUnresolvedIssues(Release release) throws IOException {
-        return findIssues("is empty", release);
+        return findIssues(release).stream().filter(issue -> issue.getResolution() == null).collect(Collectors.toList());
     }
 
     public List<Issue> findFixedIssues(Release release) throws IOException {
-        return findIssues("is not empty", release);
+        return findIssues(release).stream().filter(issue -> issue.getResolution() != null).collect(Collectors.toList());
     }
 
-    private List<Issue> findIssues(String resolution, Release release) throws IOException {
+    private void closeIssues(List<Issue> issues) throws IOException {
+        List<Promise<Issue>> closedIssues = new ArrayList<>();
+        for (Issue issue : issues) {
+            closedIssues.add(getCloseTransition(issue).then(closeTransition -> closeIssue(issue, closeTransition.getValue())));
+        }
+        Promise<List<Issue>> closedFixedIssues = promiseFactory.all(closedIssues);
+        Throwable failed;
+        try {
+            failed = closedFixedIssues.getFailure();
+            if (failed != null) {
+                if (failed instanceof FailedPromisesException) {
+                    FailedPromisesException failedPromisesException = (FailedPromisesException) failed;
+                    StringBuilder failureMessages = new StringBuilder();
+                    for (Promise<?> promise : failedPromisesException.getFailedPromises()) {
+                        failureMessages.append(promise.getFailure().getMessage()).append("\n");
+                    }
+                    throw new IOException("Unable to close the following issues:\n" + failureMessages.toString());
+                } else {
+                    throw new IOException(failed);
+                }
+            }
+        } catch (InterruptedException e) {
+            throw new IOException(e);
+        }
+    }
+
+    public void release(Release release) throws IOException {
+        List<Issue> issues = findIssues(release);
+        List<Issue> unresolvedIssues = new ArrayList<>();
+        issues.forEach(issue -> {
+            if (issue.getResolution() == null) {
+                unresolvedIssues.add(issue);
+            }
+        });
+        if (unresolvedIssues.size() == 0) {
+            closeIssues(issues);
+            Version version = find(release);
+            HttpPut put = newPut("version/" + version.getId());
+            StringWriter w = new StringWriter();
+            try (JsonWriter jw = new Gson().newJsonWriter(w)) {
+                jw.beginObject().name("released").value(true).name("releaseDate").value(dateProvider.getCurrentDateForJiraRelease()).endObject();
+            }
+            put.setEntity(new StringEntity(w.toString()));
+            try (CloseableHttpClient client = httpClientFactory.newClient()) {
+                try (CloseableHttpResponse response = client.execute(put, httpClientFactory.newPreemptiveAuthenticationContext())) {
+                    int statusCode = response.getStatusLine().getStatusCode();
+                    if (statusCode != 200) {
+                        throw new IOException(String.format("Unable to mark %s as released. Got status code %d.", release.getFullName(),
+                                statusCode));
+                    }
+                }
+            }
+        } else {
+            String report =
+                    unresolvedIssues.stream().map(issue -> String.format("%s/browse/%s", jiraURL, issue.getKey())).collect(Collectors.joining(System.lineSeparator()));
+            throw new IllegalStateException("The following issues are not fixed:\n" + report);
+        }
+    }
+
+    private List<Issue> findIssues(Release release) throws IOException {
         try {
             HttpGet get = newGet("search");
             URIBuilder builder = new URIBuilder(get.getURI());
             builder.addParameter("jql",
-                    String.format("project = %s AND resolution %s AND fixVersion = \"%s\"", PROJECT_KEY, resolution, release.getName()));
-            builder.addParameter("fields", "summary");
+                    String.format("project = %s AND fixVersion = \"%s\"", PROJECT_KEY, release.getName()));
+            builder.addParameter("fields", "summary,status,resolution");
             get.setURI(builder.build());
 
             try (CloseableHttpClient client = httpClientFactory.newClient()) {
@@ -255,12 +334,6 @@ public class VersionClient {
         }
     }
         
-    private HttpGet newGet(String suffix) {
-        HttpGet get = new HttpGet(jiraUrlPrefix + suffix);
-        get.addHeader(HttpHeaders.ACCEPT, CONTENT_TYPE_JSON);
-        return get;
-    }
-    
     private void populateRelatedIssuesCount(CloseableHttpClient client, Version version) throws IOException {
         
         HttpGet get = newGet("version/" + version.getId() +"/relatedIssueCounts");
@@ -341,4 +414,54 @@ public class VersionClient {
             throw new RuntimeException(e);
         }
     }
+
+    private Promise<Transition> getCloseTransition(Issue issue) {
+        HttpGet get = newGet("issue/" + issue.getId() + "/transitions");
+        try {
+            try (CloseableHttpClient client = httpClientFactory.newClient()) {
+                try (CloseableHttpResponse getResponse = client.execute(get)) {
+                    try (InputStream getContent = getResponse.getEntity().getContent();
+                         InputStreamReader getReader = new InputStreamReader(getContent)) {
+                        if (getResponse.getStatusLine().getStatusCode() != 200) {
+                            throw newException(getResponse, getReader);
+                        }
+                        Gson gson = new Gson();
+                        List<Transition> transitions = gson.fromJson(getReader, Transitions.class).getTransitions();
+                        Optional<Transition> transition = transitions.stream().filter(t -> "Close Issue".equals(t.getName())).findFirst();
+                        if (transition.isPresent()) {
+                            return promiseFactory.resolved(transition.get());
+                        } else {
+                            return promiseFactory
+                                    .failed(new IllegalStateException(String.format("Issue %s/browse/%s cannot be closed - missing Close " +
+                                                    "transition.", jiraURL,
+                                            issue.getKey())));
+                        }
+                    }
+                }
+            }
+        } catch (Exception e) {
+            return promiseFactory.failed(e);
+        }
+    }
+
+    private Promise<Issue> closeIssue(Issue issue, Transition closeTransition) {
+        HttpPost post = newPost("issue/" + issue.getId() + "/transitions");
+        StringWriter w = new StringWriter();
+        try (JsonWriter jw = new Gson().newJsonWriter(w)) {
+            jw.beginObject().name("transition").beginObject().name("id").value(closeTransition.getId()).endObject().endObject();
+            post.setEntity(new StringEntity(w.toString()));
+            try (CloseableHttpClient client = httpClientFactory.newClient()) {
+                try (CloseableHttpResponse postResponse = client.execute(post, httpClientFactory.newPreemptiveAuthenticationContext())) {
+                    if (postResponse.getStatusLine().getStatusCode() == 204) {
+                        return promiseFactory.resolved(issue);
+                    } else {
+                        return promiseFactory.failed(new RuntimeException(String.format("Unable to close issue %s/browse/%s - got status code %d.",
+                         jiraURL, issue.getKey(), postResponse.getStatusLine().getStatusCode())));
+                    }
+                }
+            }
+        } catch (IOException e) {
+            return promiseFactory.failed(e);
+        }
+    }
 }
diff --git a/src/main/java/org/apache/sling/cli/impl/release/ReleaseJiraVersionCommand.java b/src/main/java/org/apache/sling/cli/impl/release/ReleaseJiraVersionCommand.java
new file mode 100644
index 0000000..cbdde72
--- /dev/null
+++ b/src/main/java/org/apache/sling/cli/impl/release/ReleaseJiraVersionCommand.java
@@ -0,0 +1,105 @@
+/*~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ ~ Licensed to the Apache Software Foundation (ASF) under one
+ ~ or more contributor license agreements.  See the NOTICE file
+ ~ distributed with this work for additional information
+ ~ regarding copyright ownership.  The ASF licenses this file
+ ~ to you 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 org.apache.sling.cli.impl.release;
+
+import java.util.List;
+import java.util.Set;
+
+import org.apache.sling.cli.impl.Command;
+import org.apache.sling.cli.impl.ExecutionMode;
+import org.apache.sling.cli.impl.InputOption;
+import org.apache.sling.cli.impl.UserInput;
+import org.apache.sling.cli.impl.jira.Issue;
+import org.apache.sling.cli.impl.jira.VersionClient;
+import org.apache.sling.cli.impl.nexus.RepositoryService;
+import org.apache.sling.cli.impl.nexus.StagingRepository;
+import org.osgi.service.component.annotations.Component;
+import org.osgi.service.component.annotations.Reference;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import picocli.CommandLine;
+
+@Component(service = Command.class,
+           property = {
+                   Command.PROPERTY_NAME_COMMAND_GROUP + "=" + ReleaseJiraVersionCommand.GROUP,
+                   Command.PROPERTY_NAME_COMMAND_NAME + "=" + ReleaseJiraVersionCommand.NAME
+           }
+)
+@CommandLine.Command(
+        name = ReleaseJiraVersionCommand.NAME,
+        description = "The found Jira versions will be marked as released with the current date. All fixed issues will be closed. Before " +
+                "running this command make sure to execute " + CreateJiraVersionCommand.NAME + " in order to move any unresolved issues " +
+                "to the next version.",
+        subcommands = CommandLine.HelpCommand.class
+)
+public class ReleaseJiraVersionCommand implements Command {
+
+    private static final Logger LOGGER = LoggerFactory.getLogger(ReleaseJiraVersionCommand.class);
+
+    static final String GROUP = "release";
+    static final String NAME = "release-jira-version";
+
+    @CommandLine.Option(names = {"-r", "--repository"}, description = "Nexus repository id", required = true)
+    private Integer repositoryId;
+
+    @Reference
+    private RepositoryService repositoryService;
+
+    @Reference
+    private VersionClient versionClient;
+
+    @CommandLine.Mixin
+    private ReusableCLIOptions reusableCLIOptions;
+
+    @Override
+    public void run() {
+        try {
+            StagingRepository repo = repositoryService.find(repositoryId);
+            Set<Release> releases = repositoryService.getReleases(repo);
+            ExecutionMode executionMode = reusableCLIOptions.executionMode;
+            LOGGER.info("The following Jira versions {} be released:{}", executionMode == ExecutionMode.DRY_RUN ? "would" : "will",
+                    System.lineSeparator());
+            for (Release release : releases) {
+                List<Issue> fixedIssues = versionClient.findFixedIssues(release);
+                LOGGER.info("{}:", release.getFullName());
+                fixedIssues.forEach(issue -> LOGGER.info("- {} - {}, Status: {}, Resolution: {}", issue.getKey(), issue.getSummary(),
+                        issue.getStatus(), issue.getResolution()));
+                LOGGER.info("");
+                boolean shouldRelease = false;
+                if (executionMode == ExecutionMode.INTERACTIVE) {
+                    InputOption answer = UserInput.yesNo(String.format("Should version %s be released?", release.getFullName()),
+                            InputOption.YES);
+                    shouldRelease = (answer == InputOption.YES);
+                } else if (executionMode == ExecutionMode.AUTO) {
+                    shouldRelease = true;
+                }
+                if (shouldRelease) {
+                    versionClient.release(release);
+                    LOGGER.info("{} was released:", release.getFullName());
+                    fixedIssues = versionClient.findFixedIssues(release);
+                    fixedIssues.forEach(issue -> LOGGER.info("- {} - {}, Status: {}, Resolution: {}", issue.getKey(), issue.getSummary(),
+                            issue.getStatus(), issue.getResolution()));
+                }
+            }
+        } catch (Exception e) {
+            LOGGER.warn("Failed executing command.", e);
+        }
+    }
+}
diff --git a/src/test/java/org/apache/sling/cli/impl/http/HttpExchangeHandler.java b/src/test/java/org/apache/sling/cli/impl/http/HttpExchangeHandler.java
index 660d34c..53bcc93 100644
--- a/src/test/java/org/apache/sling/cli/impl/http/HttpExchangeHandler.java
+++ b/src/test/java/org/apache/sling/cli/impl/http/HttpExchangeHandler.java
@@ -29,15 +29,16 @@ import com.sun.net.httpserver.HttpExchange;
 public interface HttpExchangeHandler {
 
     default void serveFileFromClasspath(HttpExchange ex, String classpathLocation) throws IOException {
-        InputStream in = getClass().getResourceAsStream(classpathLocation);
-        if ( in == null  ) {
-            ex.sendResponseHeaders(404, -1);
-            return;
-        }
-
-        ex.sendResponseHeaders(200, 0);
-        try ( OutputStream out = ex.getResponseBody() ) {
-            IOUtils.copy(in, out);
+        try (InputStream in = getClass().getResourceAsStream(classpathLocation)) {
+            if (in == null) {
+                ex.sendResponseHeaders(404, -1);
+                return;
+            }
+
+            ex.sendResponseHeaders(200, 0);
+            try (OutputStream out = ex.getResponseBody()) {
+                IOUtils.copy(in, out);
+            }
         }
     }
 
diff --git a/src/test/java/org/apache/sling/cli/impl/jira/EditVersionJiraAction.java b/src/test/java/org/apache/sling/cli/impl/jira/EditVersionJiraAction.java
new file mode 100644
index 0000000..b8f4749
--- /dev/null
+++ b/src/test/java/org/apache/sling/cli/impl/jira/EditVersionJiraAction.java
@@ -0,0 +1,98 @@
+/*~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ ~ Licensed to the Apache Software Foundation (ASF) under one
+ ~ or more contributor license agreements.  See the NOTICE file
+ ~ distributed with this work for additional information
+ ~ regarding copyright ownership.  The ASF licenses this file
+ ~ to you 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 org.apache.sling.cli.impl.jira;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.lang.reflect.Type;
+import java.util.List;
+import java.util.Optional;
+import java.util.regex.Pattern;
+
+import org.apache.sling.cli.impl.DateProvider;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import com.google.gson.Gson;
+import com.google.gson.reflect.TypeToken;
+import com.sun.net.httpserver.HttpExchange;
+
+public class EditVersionJiraAction implements JiraAction {
+
+    private static final Logger LOGGER = LoggerFactory.getLogger(EditVersionJiraAction.class);
+    private static final Pattern VERSION_ID = Pattern.compile("/jira/rest/api/2/version/\\d+$");
+
+    @Override
+    public boolean tryHandle(HttpExchange ex) throws IOException {
+        if (!VERSION_ID.matcher(ex.getRequestURI().getPath()).matches()) {
+            return false;
+        }
+        String versionIdString = ex.getRequestURI().getPath().substring(25);
+        try {
+            int versionId = Integer.parseInt(versionIdString);
+            try (InputStream in = getClass().getResourceAsStream("/jira/versions.json")) {
+                if (in == null) {
+                    ex.sendResponseHeaders(404, -1);
+                    return true;
+                }
+                try (InputStreamReader reader = new InputStreamReader(in)) {
+                    Gson gson = new Gson();
+                    Type collectionType = TypeToken.getParameterized(List.class, Version.class).getType();
+                    List<Version> versions = gson.fromJson(reader, collectionType);
+                    Optional<Version> versionHolder = versions.stream().filter(v -> versionId == v.getId()).findFirst();
+                    if (versionHolder.isEmpty()) {
+                        ex.sendResponseHeaders(404, -1);
+                    } else {
+                        if ("PUT".equals(ex.getRequestMethod())) {
+                            // version change
+                            Version version = versionHolder.get();
+                            try (InputStreamReader requestReader = new InputStreamReader(ex.getRequestBody())) {
+                                VersionToUpdate versionToUpdate = gson.fromJson(requestReader, VersionToUpdate.class);
+                                DateProvider dateProvider = new DateProvider();
+                                if (versionToUpdate.released != version.isReleased() && versionToUpdate.released &&
+                                        dateProvider.getCurrentDateForJiraRelease().equals(versionToUpdate.releaseDate) &&
+                                        version.getReleaseDate() == null) {
+                                    ex.sendResponseHeaders(200, -1);
+                                }
+                            }
+                        }
+                        ex.sendResponseHeaders(406, -1);
+                    }
+                }
+            }
+
+        } catch (NumberFormatException e) {
+            LOGGER.error("Unable to parse version id from " + ex.getRequestURI().getPath(), e);
+            ex.sendResponseHeaders(400, -1);
+        }
+        return true;
+    }
+
+    @SuppressWarnings("unused")
+    static class VersionToUpdate {
+        private String description;
+        private String name;
+        private boolean archived;
+        private boolean released;
+        private String releaseDate;
+        private String userReleaseDate;
+        private String projectId;
+    }
+}
diff --git a/src/test/java/org/apache/sling/cli/impl/jira/IssuesSearchJiraAction.java b/src/test/java/org/apache/sling/cli/impl/jira/IssuesSearchJiraAction.java
index 13bed5d..7783cbe 100644
--- a/src/test/java/org/apache/sling/cli/impl/jira/IssuesSearchJiraAction.java
+++ b/src/test/java/org/apache/sling/cli/impl/jira/IssuesSearchJiraAction.java
@@ -28,8 +28,9 @@ import com.sun.net.httpserver.HttpExchange;
 
 public class IssuesSearchJiraAction implements JiraAction {
     
-    private static final String UNRESOLVED_QUERY = "project = SLING AND resolution is empty AND fixVersion = \"Committer CLI 1.0.0\"";
-    private static final String FIXED_QUERY = "project = SLING AND resolution is not empty AND fixVersion = \"Committer CLI 1.0.0\"";
+    private static final String COMMITTER_CLI_1_0_0_QUERY = "project = SLING AND fixVersion = \"Committer CLI 1.0.0\"";
+    private static final String TRANSITIONS_1_0_0_QUERY = "project = SLING AND fixVersion = \"Transitions 1.0.0\"";
+    private static final String TRANSITIONS_2_0_0_QUERY = "project = SLING AND fixVersion = \"Transitions 2.0.0\"";
 
     @Override
     public boolean tryHandle(HttpExchange ex) throws IOException {
@@ -42,17 +43,20 @@ public class IssuesSearchJiraAction implements JiraAction {
         
         for ( NameValuePair pair : parsed ) {
             if ( "jql".equals(pair.getName())) {
-                if (UNRESOLVED_QUERY.equals(pair.getValue())) {
-                    serveFileFromClasspath(ex, "/jira/search/unresolved-committer-cli-1.0.0.json");
+                if (COMMITTER_CLI_1_0_0_QUERY.equals(pair.getValue())) {
+                    serveFileFromClasspath(ex, "/jira/search/committer-cli-1.0.0.json");
                     return true;
-                } else if (FIXED_QUERY.equals(pair.getValue())) {
-                    serveFileFromClasspath(ex, "/jira/search/fixed-committer-cli-1.0.0.json");
+                } else if (TRANSITIONS_1_0_0_QUERY.equals(pair.getValue())) {
+                    serveFileFromClasspath(ex, "/jira/search/transitions-1.0.0.json");
+                    return true;
+                } else if (TRANSITIONS_2_0_0_QUERY.equals(pair.getValue())) {
+                    serveFileFromClasspath(ex, "/jira/search/transitions-2.0.0.json");
                     return true;
                 }
             }
         }
         error(ex, new Gson(), er -> er.getErrorMessages().add("Unable to run unknown JQL query, available ones are [" +
-                UNRESOLVED_QUERY + "," + FIXED_QUERY +"]"));
+                COMMITTER_CLI_1_0_0_QUERY + "," + TRANSITIONS_1_0_0_QUERY +"]"));
         
         return true;
     }
diff --git a/src/test/java/org/apache/sling/cli/impl/jira/MockJira.java b/src/test/java/org/apache/sling/cli/impl/jira/MockJira.java
index 73ccb5b..95e4827 100644
--- a/src/test/java/org/apache/sling/cli/impl/jira/MockJira.java
+++ b/src/test/java/org/apache/sling/cli/impl/jira/MockJira.java
@@ -19,6 +19,8 @@ package org.apache.sling.cli.impl.jira;
 import java.net.InetSocketAddress;
 import java.util.ArrayList;
 import java.util.List;
+import java.util.Set;
+import java.util.regex.Pattern;
 
 import org.apache.sling.cli.impl.http.HttpExchangeHandler;
 import org.junit.rules.ExternalResource;
@@ -34,6 +36,7 @@ public class MockJira extends ExternalResource {
     
     static final String AUTH_USER = "asf-user";
     static final String AUTH_PWD = "asf-password";
+    private static final Set<Pattern> GET_PATHS_REQUIRING_AUTH = Set.of(Pattern.compile("/jira/rest/api/2/issue/\\d+/transitions"));
     
     public static void main(String[] args) throws Throwable {
         
@@ -57,9 +60,14 @@ public class MockJira extends ExternalResource {
             
             @Override
             public Result authenticate(HttpExchange t) {
-                // get requests are never authenticated
-                if ( t.getRequestMethod().contentEquals("GET") )
+                if (t.getRequestMethod().contentEquals("GET")) {
+                    for (Pattern pathPattern : GET_PATHS_REQUIRING_AUTH) {
+                        if (pathPattern.matcher(t.getHttpContext().getPath()).matches()) {
+                            return super.authenticate(t);
+                        }
+                    }
                     return new Authenticator.Success(new HttpPrincipal("anonymous", getClass().getSimpleName()));
+                }
                 return super.authenticate(t);
             }
         });
@@ -69,7 +77,9 @@ public class MockJira extends ExternalResource {
         actions.add(new GetRelatedIssueCountsForVersionsJiraAction());
         actions.add(new CreateVersionJiraAction());
         actions.add(new IssuesSearchJiraAction());
-        
+        actions.add(new TransitionsJiraAction());
+        actions.add(new EditVersionJiraAction());
+
         // fallback, always executed
         actions.add(ex -> {
             ex.sendResponseHeaders(400, -1);
diff --git a/src/test/java/org/apache/sling/cli/impl/jira/TransitionsJiraAction.java b/src/test/java/org/apache/sling/cli/impl/jira/TransitionsJiraAction.java
new file mode 100644
index 0000000..a96947f
--- /dev/null
+++ b/src/test/java/org/apache/sling/cli/impl/jira/TransitionsJiraAction.java
@@ -0,0 +1,66 @@
+/*~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ ~ Licensed to the Apache Software Foundation (ASF) under one
+ ~ or more contributor license agreements.  See the NOTICE file
+ ~ distributed with this work for additional information
+ ~ regarding copyright ownership.  The ASF licenses this file
+ ~ to you 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 org.apache.sling.cli.impl.jira;
+
+import java.io.IOException;
+import java.io.InputStreamReader;
+import java.util.regex.Pattern;
+
+import com.google.gson.Gson;
+import com.google.gson.JsonParseException;
+import com.sun.net.httpserver.HttpExchange;
+
+public class TransitionsJiraAction implements JiraAction {
+
+    private static final Pattern TRANSITIONS = Pattern.compile("/jira/rest/api/2/issue/\\d+/transitions");
+    private static final Pattern RETURN_NO_TRANSITIONS = Pattern.compile("/jira/rest/api/2/issue/(1|2|3)/transitions");
+
+    @Override
+    public boolean tryHandle(HttpExchange ex) throws IOException {
+        if (!TRANSITIONS.matcher(ex.getRequestURI().getPath()).matches()) {
+            return false;
+        }
+        if (ex.getRequestMethod().equals("GET")) {
+            if (RETURN_NO_TRANSITIONS.matcher(ex.getRequestURI().getPath()).matches()) {
+                serveFileFromClasspath(ex, "/jira/transitions/no-transitions.json");
+            } else {
+                serveFileFromClasspath(ex, "/jira/transitions/transitions.json");
+            }
+        } else if (ex.getRequestMethod().equals("POST")) {
+            Gson gson = new Gson();
+            try ( InputStreamReader reader = new InputStreamReader(ex.getRequestBody())) {
+                TransitionToExecute transitionToExecute = gson.fromJson(reader, TransitionToExecute.class);
+                if (701 == transitionToExecute.transition.getId()) {
+                    ex.sendResponseHeaders(204, -1);
+                } else {
+                    ex.sendResponseHeaders(400, -1);
+                }
+            } catch (JsonParseException e) {
+                ex.sendResponseHeaders(400, -1);
+            }
+        }
+        return true;
+    }
+
+    private static class TransitionToExecute {
+       private Transition transition;
+    }
+
+
+}
diff --git a/src/test/java/org/apache/sling/cli/impl/jira/VersionClientTest.java b/src/test/java/org/apache/sling/cli/impl/jira/VersionClientTest.java
index 5ad42f8..8f09c23 100644
--- a/src/test/java/org/apache/sling/cli/impl/jira/VersionClientTest.java
+++ b/src/test/java/org/apache/sling/cli/impl/jira/VersionClientTest.java
@@ -16,18 +16,13 @@
  */
 package org.apache.sling.cli.impl.jira;
 
-import static org.hamcrest.Matchers.equalTo;
-import static org.hamcrest.Matchers.hasSize;
-import static org.hamcrest.Matchers.notNullValue;
-import static org.hamcrest.Matchers.nullValue;
-import static org.junit.Assert.assertThat;
-
 import java.io.IOException;
 import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
 
 import org.apache.sling.cli.impl.CredentialsService;
+import org.apache.sling.cli.impl.DateProvider;
 import org.apache.sling.cli.impl.http.HttpClientFactory;
 import org.apache.sling.cli.impl.junit.SystemPropertiesRule;
 import org.apache.sling.cli.impl.release.Release;
@@ -36,6 +31,14 @@ import org.junit.Before;
 import org.junit.Rule;
 import org.junit.Test;
 
+import static org.hamcrest.Matchers.equalTo;
+import static org.hamcrest.Matchers.hasSize;
+import static org.hamcrest.Matchers.notNullValue;
+import static org.hamcrest.Matchers.nullValue;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertThat;
+import static org.junit.Assert.assertTrue;
+
 public class VersionClientTest {
 
     private static final Map<String, String> SYSTEM_PROPS = new HashMap<>();
@@ -52,15 +55,15 @@ public class VersionClientTest {
     
     @Rule
     public final MockJira mockJira = new MockJira();
-    
+
     private VersionClient versionClient;
     
     @Before
     public void prepareDependencies() {
-        
         context.registerInjectActivateService(new CredentialsService());
+        context.registerInjectActivateService(new DateProvider());
         context.registerInjectActivateService(new HttpClientFactory(), "jira.host", "localhost", "jira.port", mockJira.getBoundPort());
-        versionClient = context.registerInjectActivateService(new VersionClient(), "jira.url.prefix", "http://localhost:" + mockJira.getBoundPort() + "/jira/rest/api/2/");
+        versionClient = context.registerInjectActivateService(new VersionClient(), "jira.url", "http://localhost:" + mockJira.getBoundPort() + "/jira");
     }
     
     @Test
@@ -111,7 +114,9 @@ public class VersionClientTest {
         
         assertThat(issues, hasSize(2));
         assertThat(issues.get(0).getKey(), equalTo("SLING-8338"));
+        assertThat(issues.get(0).getStatus(), equalTo("Open"));
         assertThat(issues.get(1).getKey(), equalTo("SLING-8337"));
+        assertThat(issues.get(1).getStatus(), equalTo("Open"));
     }
 
     @Test
@@ -120,11 +125,39 @@ public class VersionClientTest {
 
         assertThat(issues, hasSize(7));
         assertThat(issues.get(0).getKey(), equalTo("SLING-8707"));
+        assertThat(issues.get(0).getStatus(), equalTo("Resolved"));
         assertThat(issues.get(1).getKey(), equalTo("SLING-8699"));
+        assertThat(issues.get(1).getStatus(), equalTo("Resolved"));
         assertThat(issues.get(2).getKey(), equalTo("SLING-8395"));
+        assertThat(issues.get(2).getStatus(), equalTo("Resolved"));
         assertThat(issues.get(3).getKey(), equalTo("SLING-8394"));
+        assertThat(issues.get(3).getStatus(), equalTo("Resolved"));
         assertThat(issues.get(4).getKey(), equalTo("SLING-8393"));
+        assertThat(issues.get(4).getStatus(), equalTo("Resolved"));
         assertThat(issues.get(5).getKey(), equalTo("SLING-8392"));
+        assertThat(issues.get(5).getStatus(), equalTo("Resolved"));
         assertThat(issues.get(6).getKey(), equalTo("SLING-8338"));
+        assertThat(issues.get(6).getStatus(), equalTo("Resolved"));
     }
+
+    @Test
+    public void releaseWithUnresolvedIssues() throws IOException {
+        Release release = Release.fromString("Committer CLI 1.0.0").get(0);
+        Exception exception = null;
+        try {
+            versionClient.release(release);
+        } catch (IllegalStateException e) {
+            exception = e;
+        }
+        assertNotNull("The VersionClient should not have allowed a release with unresolved issues.", exception);
+        assertTrue("SLING-8337 should have been reported as unresolved.", exception.getMessage().contains("SLING-8337"));
+        assertTrue("SLING-8338 should have been reported as unresolved.", exception.getMessage().contains("SLING-8338"));
+    }
+
+    @Test
+    public void release() throws IOException {
+        Release release = Release.fromString("Transitions 2.0.0").get(0);
+        versionClient.release(release);
+    }
+
 }
diff --git a/src/test/resources/jira/relatedIssueCounts/0.json b/src/test/resources/jira/relatedIssueCounts/0.json
new file mode 100644
index 0000000..c4cfeb5
--- /dev/null
+++ b/src/test/resources/jira/relatedIssueCounts/0.json
@@ -0,0 +1,6 @@
+{
+    "self": "https://issues.apache.org/jira/rest/api/2/version/0",
+    "issuesFixedCount": 3,
+    "issuesAffectedCount": 3,
+    "issueCountWithCustomFieldsShowingVersion": 0
+}
diff --git a/src/test/resources/jira/search/fixed-committer-cli-1.0.0.json b/src/test/resources/jira/search/committer-cli-1.0.0.json
similarity index 50%
rename from src/test/resources/jira/search/fixed-committer-cli-1.0.0.json
rename to src/test/resources/jira/search/committer-cli-1.0.0.json
index dbd6685..84916c5 100644
--- a/src/test/resources/jira/search/fixed-committer-cli-1.0.0.json
+++ b/src/test/resources/jira/search/committer-cli-1.0.0.json
@@ -2,7 +2,7 @@
     "expand": "schema,names",
     "startAt": 0,
     "maxResults": 50,
-    "total": 7,
+    "total": 9,
     "issues": [
         {
             "expand": "operations,versionedRepresentations,editmeta,changelog,renderedFields",
@@ -10,7 +10,13 @@
             "self": "https://issues.apache.org/jira/rest/api/2/issue/13256564",
             "key": "SLING-8707",
             "fields": {
-                "summary": "Try to map a staging Nexus repository to a JIRA release by inspecting the POM files"
+                "summary": "Try to map a staging Nexus repository to a JIRA release by inspecting the POM files",
+                "status": {
+                    "name": "Resolved"
+                },
+                "resolution": {
+                    "name": "Fixed"
+                }
             }
         },
         {
@@ -19,7 +25,13 @@
             "self": "https://issues.apache.org/jira/rest/api/2/issue/13256303",
             "key": "SLING-8699",
             "fields": {
-                "summary": "Create sub-command to moving artifacts to dist.apache.org"
+                "summary": "Create sub-command to moving artifacts to dist.apache.org",
+                "status": {
+                    "name": "Resolved"
+                },
+                "resolution": {
+                    "name": "Fixed"
+                }
             }
         },
         {
@@ -28,7 +40,13 @@
             "self": "https://issues.apache.org/jira/rest/api/2/issue/13231466",
             "key": "SLING-8395",
             "fields": {
-                "summary": "Investigate automatically issuing GitHub PRs with the Committer CLI"
+                "summary": "Investigate automatically issuing GitHub PRs with the Committer CLI",
+                "status": {
+                    "name": "Resolved"
+                },
+                "resolution": {
+                    "name": "Fixed"
+                }
             }
         },
         {
@@ -37,7 +55,13 @@
             "self": "https://issues.apache.org/jira/rest/api/2/issue/13231465",
             "key": "SLING-8394",
             "fields": {
-                "summary": "Create sub-command to update the Sling starter when a release is made"
+                "summary": "Create sub-command to update the Sling starter when a release is made",
+                "status": {
+                    "name": "Resolved"
+                },
+                "resolution": {
+                    "name": "Fixed"
+                }
             }
         },
         {
@@ -46,7 +70,13 @@
             "self": "https://issues.apache.org/jira/rest/api/2/issue/13231464",
             "key": "SLING-8393",
             "fields": {
-                "summary": "Create sub-command to update the Sling website when a release is made"
+                "summary": "Create sub-command to update the Sling website when a release is made",
+                "status": {
+                    "name": "Resolved"
+                },
+                "resolution": {
+                    "name": "Fixed"
+                }
             }
         },
         {
@@ -55,7 +85,13 @@
             "self": "https://issues.apache.org/jira/rest/api/2/issue/13231459",
             "key": "SLING-8392",
             "fields": {
-                "summary": "Create sub-command to manage the Jira update when promoting a release"
+                "summary": "Create sub-command to manage the Jira update when promoting a release",
+                "status": {
+                    "name": "Resolved"
+                },
+                "resolution": {
+                    "name": "Fixed"
+                }
             }
         },
         {
@@ -64,7 +100,39 @@
             "self": "https://issues.apache.org/jira/rest/api/2/issue/13225243",
             "key": "SLING-8338",
             "fields": {
-                "summary": "Create sub-command to manage the Nexus stage repository release when promoting a release"
+                "summary": "Create sub-command to manage the Nexus stage repository release when promoting a release",
+                "status": {
+                    "name": "Resolved"
+                },
+                "resolution": {
+                    "name": "Fixed"
+                }
+            }
+        },
+        {
+            "expand": "operations,versionedRepresentations,editmeta,changelog,renderedFields",
+            "id": "13225243",
+            "self": "https://issues.apache.org/jira/rest/api/2/issue/13225243",
+            "key": "SLING-8338",
+            "fields": {
+                "summary": "Create sub-command to manage the Nexus stage repository release when promoting a release",
+                "status": {
+                    "name": "Open"
+                },
+                "resolution": null
+            }
+        },
+        {
+            "expand": "operations,versionedRepresentations,editmeta,changelog,renderedFields",
+            "id": "13224868",
+            "self": "https://issues.apache.org/jira/rest/api/2/issue/13224868",
+            "key": "SLING-8337",
+            "fields": {
+                "summary": "Create sub-command to manage the Jira update when promoting a release",
+                "status": {
+                    "name": "Open"
+                },
+                "resolution": null
             }
         }
     ]
diff --git a/src/test/resources/jira/search/transitions-1.0.0.json b/src/test/resources/jira/search/transitions-1.0.0.json
new file mode 100644
index 0000000..bb61a81
--- /dev/null
+++ b/src/test/resources/jira/search/transitions-1.0.0.json
@@ -0,0 +1,53 @@
+{
+    "expand": "schema,names",
+    "startAt": 0,
+    "maxResults": 50,
+    "total": 3,
+    "issues": [
+        {
+            "expand": "operations,versionedRepresentations,editmeta,changelog,renderedFields",
+            "id": "1",
+            "self": "https://issues.apache.org/jira/rest/api/2/issue/1",
+            "key": "SLING-0001",
+            "fields": {
+                "summary": "Test Transitions 1",
+                "status": {
+                    "name": "Resolved"
+                },
+                "resolution": {
+                    "name": "Fixed"
+                }
+            }
+        },
+        {
+            "expand": "operations,versionedRepresentations,editmeta,changelog,renderedFields",
+            "id": "2",
+            "self": "https://issues.apache.org/jira/rest/api/2/issue/2",
+            "key": "SLING-0002",
+            "fields": {
+                "summary": "Test Transitions 2",
+                "status": {
+                    "name": "Resolved"
+                },
+                "resolution": {
+                    "name": "Fixed"
+                }
+            }
+        },
+        {
+            "expand": "operations,versionedRepresentations,editmeta,changelog,renderedFields",
+            "id": "3",
+            "self": "https://issues.apache.org/jira/rest/api/2/issue/3",
+            "key": "SLING-0003",
+            "fields": {
+                "summary": "Test Transitions 3",
+                "status": {
+                    "name": "Resolved"
+                },
+                "resolution": {
+                    "name": "Fixed"
+                }
+            }
+        }
+    ]
+}
diff --git a/src/test/resources/jira/search/transitions-2.0.0.json b/src/test/resources/jira/search/transitions-2.0.0.json
new file mode 100644
index 0000000..08f92aa
--- /dev/null
+++ b/src/test/resources/jira/search/transitions-2.0.0.json
@@ -0,0 +1,53 @@
+{
+    "expand": "schema,names",
+    "startAt": 0,
+    "maxResults": 50,
+    "total": 3,
+    "issues": [
+        {
+            "expand": "operations,versionedRepresentations,editmeta,changelog,renderedFields",
+            "id": "4",
+            "self": "https://issues.apache.org/jira/rest/api/2/issue/4",
+            "key": "SLING-0004",
+            "fields": {
+                "summary": "Test Transitions 4",
+                "status": {
+                    "name": "Resolved"
+                },
+                "resolution": {
+                    "name": "Fixed"
+                }
+            }
+        },
+        {
+            "expand": "operations,versionedRepresentations,editmeta,changelog,renderedFields",
+            "id": "5",
+            "self": "https://issues.apache.org/jira/rest/api/2/issue/5",
+            "key": "SLING-0005",
+            "fields": {
+                "summary": "Test Transitions 5",
+                "status": {
+                    "name": "Resolved"
+                },
+                "resolution": {
+                    "name": "Fixed"
+                }
+            }
+        },
+        {
+            "expand": "operations,versionedRepresentations,editmeta,changelog,renderedFields",
+            "id": "6",
+            "self": "https://issues.apache.org/jira/rest/api/2/issue/6",
+            "key": "SLING-0006",
+            "fields": {
+                "summary": "Test Transitions 6",
+                "status": {
+                    "name": "Resolved"
+                },
+                "resolution": {
+                    "name": "Fixed"
+                }
+            }
+        }
+    ]
+}
diff --git a/src/test/resources/jira/search/unresolved-committer-cli-1.0.0.json b/src/test/resources/jira/search/unresolved-committer-cli-1.0.0.json
deleted file mode 100644
index 6a01267..0000000
--- a/src/test/resources/jira/search/unresolved-committer-cli-1.0.0.json
+++ /dev/null
@@ -1,26 +0,0 @@
-{
-  "expand": "schema,names",
-  "startAt": 0,
-  "maxResults": 50,
-  "total": 2,
-  "issues": [
-    {
-      "expand": "operations,versionedRepresentations,editmeta,changelog,renderedFields",
-      "id": "13225243",
-      "self": "https://issues.apache.org/jira/rest/api/2/issue/13225243",
-      "key": "SLING-8338",
-      "fields": {
-        "summary": "Create sub-command to manage the Nexus stage repository release when promoting a release"
-      }
-    },
-    {
-      "expand": "operations,versionedRepresentations,editmeta,changelog,renderedFields",
-      "id": "13224868",
-      "self": "https://issues.apache.org/jira/rest/api/2/issue/13224868",
-      "key": "SLING-8337",
-      "fields": {
-        "summary": "Create sub-command to manage the Jira update when promoting a release"
-      }
-    }
-  ]
-}
diff --git a/src/test/resources/jira/transitions/no-transitions.json b/src/test/resources/jira/transitions/no-transitions.json
new file mode 100644
index 0000000..496bc81
--- /dev/null
+++ b/src/test/resources/jira/transitions/no-transitions.json
@@ -0,0 +1,5 @@
+{
+    "expand"     : "transitions",
+    "transitions": [
+    ]
+}
diff --git a/src/test/resources/jira/transitions/transitions.json b/src/test/resources/jira/transitions/transitions.json
new file mode 100644
index 0000000..950f412
--- /dev/null
+++ b/src/test/resources/jira/transitions/transitions.json
@@ -0,0 +1,41 @@
+{
+    "expand"     : "transitions",
+    "transitions": [
+        {
+            "id"  : "701",
+            "name": "Close Issue",
+            "to"  : {
+                "self"          : "https://issues.apache.org/jira/rest/api/2/status/6",
+                "description"   : "The issue is considered finished, the resolution is correct. Issues which are not closed can be reopened.",
+                "iconUrl"       : "https://issues.apache.org/jira/images/icons/statuses/closed.png",
+                "name"          : "Closed",
+                "id"            : "6",
+                "statusCategory": {
+                    "self"     : "https://issues.apache.org/jira/rest/api/2/statuscategory/3",
+                    "id"       : 3,
+                    "key"      : "done",
+                    "colorName": "green",
+                    "name"     : "Done"
+                }
+            }
+        },
+        {
+            "id"  : "3",
+            "name": "Reopen Issue",
+            "to"  : {
+                "self"          : "https://issues.apache.org/jira/rest/api/2/status/4",
+                "description"   : "This issue was once resolved, but the resolution was deemed incorrect. From here issues are either marked assigned or resolved.",
+                "iconUrl"       : "https://issues.apache.org/jira/images/icons/statuses/reopened.png",
+                "name"          : "Reopened",
+                "id"            : "4",
+                "statusCategory": {
+                    "self"     : "https://issues.apache.org/jira/rest/api/2/statuscategory/2",
+                    "id"       : 2,
+                    "key"      : "new",
+                    "colorName": "blue-gray",
+                    "name"     : "To Do"
+                }
+            }
+        }
+    ]
+}
diff --git a/src/test/resources/jira/versions.json b/src/test/resources/jira/versions.json
index d7e5226..a90e94d 100644
--- a/src/test/resources/jira/versions.json
+++ b/src/test/resources/jira/versions.json
@@ -2083,5 +2083,16 @@
     "releaseDate": "2010-12-13",
     "userReleaseDate": "13/Dec/10",
     "projectId": 12310710
+  },
+  {
+    "self": "https://issues.apache.org/jira/rest/api/2/version/0",
+    "id": "0",
+    "description": "Maintenance release",
+    "name": "Transitions 2.0.0",
+    "archived": false,
+    "released": false,
+    "releaseDate": null,
+    "userReleaseDate": null,
+    "projectId": 12310710
   }
 ]