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

royteeuwen pushed a commit to branch feature/SLING-13253-act-by-name
in repository 
https://gitbox.apache.org/repos/asf/sling-org-apache-sling-committer-cli.git

commit 343b26e06629d1fcf6016a702fdec800232fe646
Author: Roy Teeuwen <[email protected]>
AuthorDate: Wed Jul 8 20:49:53 2026 +0200

    SLING-13253 - release: act on a release by name
    
    Lets the post-vote release commands (create-jira-version, 
release-jira-version,
    update-local-site, update-reporter) accept a release by --release name in
    addition to a staging repository id, and adds tests for the JIRA-version and
    verify commands. A CommandProcessor test guards that a multi-word release 
name
    passed as a quoted argument is parsed intact.
    
    Split out of the finalize PR (#34) since these changes are independent of 
the
    finalize command and can be merged on their own.
---
 .../cli/impl/release/CreateJiraVersionCommand.java |  29 +--
 .../impl/release/ReleaseJiraVersionCommand.java    |  29 ++-
 .../cli/impl/release/UpdateLocalSiteCommand.java   |  28 ++-
 .../cli/impl/release/UpdateReporterCommand.java    |  28 ++-
 .../sling/cli/impl/CommandProcessorTest.java       |  38 ++++
 .../impl/release/CreateJiraVersionCommandTest.java | 217 +++++++++++++++++++++
 .../release/ReleaseJiraVersionCommandTest.java     | 152 +++++++++++++++
 .../impl/release/UpdateLocalSiteCommandTest.java   | 154 +++++++++++++++
 .../impl/release/UpdateReporterCommandTest.java    |  51 +++++
 .../impl/release/VerifyReleasesCommandTest.java    | 127 ++++++++++++
 10 files changed, 827 insertions(+), 26 deletions(-)

diff --git 
a/src/main/java/org/apache/sling/cli/impl/release/CreateJiraVersionCommand.java 
b/src/main/java/org/apache/sling/cli/impl/release/CreateJiraVersionCommand.java
index 226e01a..41170dc 100644
--- 
a/src/main/java/org/apache/sling/cli/impl/release/CreateJiraVersionCommand.java
+++ 
b/src/main/java/org/apache/sling/cli/impl/release/CreateJiraVersionCommand.java
@@ -30,7 +30,6 @@ import org.apache.sling.cli.impl.jira.Issue;
 import org.apache.sling.cli.impl.jira.Version;
 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;
@@ -56,8 +55,7 @@ public class CreateJiraVersionCommand implements Command {
 
     @CommandLine.Option(
             names = {"-r", "--repository"},
-            description = "Nexus repository id",
-            required = true)
+            description = "Nexus staging repository id to derive the 
release(s) from")
     private Integer repositoryId;
 
     @Reference
@@ -67,9 +65,10 @@ public class CreateJiraVersionCommand implements Command {
     private VersionClient versionClient;
 
     @CommandLine.Option(
-            names = {"--version-name"},
-            description = "Jira version name to use",
-            required = false)
+            names = {"--release", "--version-name"},
+            description = "Release name(s) to act on, e.g. \"Apache Sling Foo 
1.2.0\" (comma-separated for multiple)."
+                    + " Use instead of --repository when the staging 
repository no longer exists,"
+                    + " e.g. after the release has been promoted.")
     private String jiraVersionName;
 
     @CommandLine.Mixin
@@ -80,7 +79,12 @@ public class CreateJiraVersionCommand implements Command {
     @Override
     public Integer call() {
         try {
-            for (Release release : releases()) {
+            Collection<Release> releases = releases();
+            if (releases.isEmpty()) {
+                logger.error("Provide either --repository or --release.");
+                return CommandLine.ExitCode.USAGE;
+            }
+            for (Release release : releases) {
                 Version version = versionClient.find(release);
                 logger.info("Found {}.", version);
                 Version successorVersion = 
versionClient.findSuccessorVersion(release);
@@ -145,9 +149,12 @@ public class CreateJiraVersionCommand implements Command {
     }
 
     private Collection<Release> releases() throws IOException {
-        if (jiraVersionName != null) return 
Release.fromString(jiraVersionName);
-
-        StagingRepository repo = repositoryService.find(repositoryId);
-        return repositoryService.getReleases(repo);
+        if (jiraVersionName != null && !jiraVersionName.isBlank()) {
+            return Release.fromString(jiraVersionName);
+        }
+        if (repositoryId == null) {
+            return List.of();
+        }
+        return 
repositoryService.getReleases(repositoryService.find(repositoryId));
     }
 }
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
index 5aa9a1c..5eaa893 100644
--- 
a/src/main/java/org/apache/sling/cli/impl/release/ReleaseJiraVersionCommand.java
+++ 
b/src/main/java/org/apache/sling/cli/impl/release/ReleaseJiraVersionCommand.java
@@ -18,6 +18,7 @@
  */
 package org.apache.sling.cli.impl.release;
 
+import java.io.IOException;
 import java.util.List;
 import java.util.Set;
 
@@ -28,7 +29,6 @@ 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;
@@ -58,10 +58,16 @@ public class ReleaseJiraVersionCommand implements Command {
 
     @CommandLine.Option(
             names = {"-r", "--repository"},
-            description = "Nexus repository id",
-            required = true)
+            description = "Nexus staging repository id to derive the 
release(s) from")
     private Integer repositoryId;
 
+    @CommandLine.Option(
+            names = {"--release"},
+            description = "Release name(s) to act on, e.g. \"Apache Sling Foo 
1.2.0\" (comma-separated for multiple)."
+                    + " Use instead of --repository when the staging 
repository no longer exists,"
+                    + " e.g. after the release has been promoted.")
+    private String releaseName;
+
     @Reference
     private RepositoryService repositoryService;
 
@@ -71,11 +77,24 @@ public class ReleaseJiraVersionCommand implements Command {
     @CommandLine.Mixin
     private ReusableCLIOptions reusableCLIOptions;
 
+    private Set<Release> resolveReleases() throws IOException {
+        if (releaseName != null && !releaseName.isBlank()) {
+            return Set.copyOf(Release.fromString(releaseName));
+        }
+        if (repositoryId == null) {
+            return Set.of();
+        }
+        return 
repositoryService.getReleases(repositoryService.find(repositoryId));
+    }
+
     @Override
     public Integer call() {
         try {
-            StagingRepository repo = repositoryService.find(repositoryId);
-            Set<Release> releases = repositoryService.getReleases(repo);
+            Set<Release> releases = resolveReleases();
+            if (releases.isEmpty()) {
+                LOGGER.error("Provide either --repository or --release.");
+                return CommandLine.ExitCode.USAGE;
+            }
             ExecutionMode executionMode = reusableCLIOptions.executionMode;
             LOGGER.info(
                     "The following Jira versions {} be released:{}",
diff --git 
a/src/main/java/org/apache/sling/cli/impl/release/UpdateLocalSiteCommand.java 
b/src/main/java/org/apache/sling/cli/impl/release/UpdateLocalSiteCommand.java
index b9a3e6e..e92de4c 100644
--- 
a/src/main/java/org/apache/sling/cli/impl/release/UpdateLocalSiteCommand.java
+++ 
b/src/main/java/org/apache/sling/cli/impl/release/UpdateLocalSiteCommand.java
@@ -28,7 +28,6 @@ import java.util.Set;
 import org.apache.sling.cli.impl.Command;
 import org.apache.sling.cli.impl.jbake.JBakeContentUpdater;
 import org.apache.sling.cli.impl.nexus.RepositoryService;
-import org.apache.sling.cli.impl.nexus.StagingRepository;
 import org.eclipse.jgit.api.Git;
 import org.eclipse.jgit.api.ResetCommand.ResetType;
 import org.eclipse.jgit.api.errors.GitAPIException;
@@ -63,18 +62,37 @@ public class UpdateLocalSiteCommand implements Command {
 
     @CommandLine.Option(
             names = {"-r", "--repository"},
-            description = "Nexus repository id",
-            required = true)
+            description = "Nexus staging repository id to derive the 
release(s) from")
     private Integer repositoryId;
 
+    @CommandLine.Option(
+            names = {"--release"},
+            description = "Release name(s) to act on, e.g. \"Apache Sling Foo 
1.2.0\" (comma-separated for multiple)."
+                    + " Use instead of --repository when the staging 
repository no longer exists,"
+                    + " e.g. after the release has been promoted.")
+    private String releaseName;
+
+    private Set<Release> resolveReleases() throws IOException {
+        if (releaseName != null && !releaseName.isBlank()) {
+            return Set.copyOf(Release.fromString(releaseName));
+        }
+        if (repositoryId == null) {
+            return Set.of();
+        }
+        return 
repositoryService.getReleases(repositoryService.find(repositoryId));
+    }
+
     @Override
     public Integer call() {
         try {
             ensureRepo();
             try (Git git = Git.open(new File(GIT_CHECKOUT))) {
 
-                StagingRepository repository = 
repositoryService.find(repositoryId);
-                Set<Release> releases = 
repositoryService.getReleases(repository);
+                Set<Release> releases = resolveReleases();
+                if (releases.isEmpty()) {
+                    logger.error("Provide either --repository or --release.");
+                    return CommandLine.ExitCode.USAGE;
+                }
 
                 JBakeContentUpdater updater = new JBakeContentUpdater();
 
diff --git 
a/src/main/java/org/apache/sling/cli/impl/release/UpdateReporterCommand.java 
b/src/main/java/org/apache/sling/cli/impl/release/UpdateReporterCommand.java
index 983618c..66e446f 100644
--- a/src/main/java/org/apache/sling/cli/impl/release/UpdateReporterCommand.java
+++ b/src/main/java/org/apache/sling/cli/impl/release/UpdateReporterCommand.java
@@ -37,7 +37,6 @@ import org.apache.sling.cli.impl.InputOption;
 import org.apache.sling.cli.impl.UserInput;
 import org.apache.sling.cli.impl.http.HttpClientFactory;
 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;
@@ -69,18 +68,37 @@ public class UpdateReporterCommand implements Command {
 
     @CommandLine.Option(
             names = {"-r", "--repository"},
-            description = "Nexus repository id",
-            required = true)
+            description = "Nexus staging repository id to derive the 
release(s) from")
     private Integer repositoryId;
 
+    @CommandLine.Option(
+            names = {"--release"},
+            description = "Release name(s) to act on, e.g. \"Apache Sling Foo 
1.2.0\" (comma-separated for multiple)."
+                    + " Use instead of --repository when the staging 
repository no longer exists,"
+                    + " e.g. after the release has been promoted.")
+    private String releaseName;
+
     @CommandLine.Mixin
     private ReusableCLIOptions reusableCLIOptions;
 
+    private Set<Release> resolveReleases() throws IOException {
+        if (releaseName != null && !releaseName.isBlank()) {
+            return Set.copyOf(Release.fromString(releaseName));
+        }
+        if (repositoryId == null) {
+            return Set.of();
+        }
+        return 
repositoryService.getReleases(repositoryService.find(repositoryId));
+    }
+
     @Override
     public Integer call() {
         try {
-            StagingRepository repository = 
repositoryService.find(repositoryId);
-            Set<Release> releases = repositoryService.getReleases(repository);
+            Set<Release> releases = resolveReleases();
+            if (releases.isEmpty()) {
+                LOGGER.error("Provide either --repository or --release.");
+                return CommandLine.ExitCode.USAGE;
+            }
             String releaseReleases = releases.size() > 1 ? "releases" : 
"release";
             switch (reusableCLIOptions.executionMode) {
                 case DRY_RUN:
diff --git a/src/test/java/org/apache/sling/cli/impl/CommandProcessorTest.java 
b/src/test/java/org/apache/sling/cli/impl/CommandProcessorTest.java
index bf31c16..4e07f4c 100644
--- a/src/test/java/org/apache/sling/cli/impl/CommandProcessorTest.java
+++ b/src/test/java/org/apache/sling/cli/impl/CommandProcessorTest.java
@@ -99,4 +99,42 @@ public class CommandProcessorTest {
         assertThat(cmd.repositoryId, equalTo(24));
         assertThat(cmd.description, equalTo("Test 'description'"));
     }
+
+    /**
+     * A multi-word option value passed as a separate, quoted argument (e.g. 
{@code --release "Scripting
+     * Core 2.1.4"}) must survive intact: the shell passes it as one argument, 
run.sh writes it on its
+     * own line, and the newline-based arg parsing must not split it on spaces.
+     */
+    @Test
+    public void multiWordOptionValuePassedAsSeparateArgument() {
+        CommandProcessor commandProcessor = new CommandProcessor() {
+            @Override
+            protected String getArgLine() {
+                return "release\n" + "mock\n" + "-r\n" + "24\n" + 
"--description\n" + "Scripting Core 2.1.4";
+            }
+
+            @Override
+            protected void stopFramework() {
+                /* ignored  */
+            }
+
+            @Override
+            protected void terminateExecution(int commandExitCode) {
+                /* ignored  */
+            }
+        };
+        MockCommand cmd = new MockCommand();
+        commandProcessor.bindCommand(
+                cmd,
+                Map.of(
+                        Command.PROPERTY_NAME_COMMAND_GROUP,
+                        MockCommand.COMMAND_GROUP,
+                        Command.PROPERTY_NAME_COMMAND_NAME,
+                        MockCommand.COMMAND_NAME));
+
+        commandProcessor.runCommand();
+
+        assertThat(cmd.invocationCount, equalTo(1));
+        assertThat(cmd.description, equalTo("Scripting Core 2.1.4"));
+    }
 }
diff --git 
a/src/test/java/org/apache/sling/cli/impl/release/CreateJiraVersionCommandTest.java
 
b/src/test/java/org/apache/sling/cli/impl/release/CreateJiraVersionCommandTest.java
new file mode 100644
index 0000000..3e23847
--- /dev/null
+++ 
b/src/test/java/org/apache/sling/cli/impl/release/CreateJiraVersionCommandTest.java
@@ -0,0 +1,217 @@
+/*
+ * 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 org.apache.commons.lang3.reflect.FieldUtils;
+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.Version;
+import org.apache.sling.cli.impl.jira.VersionClient;
+import org.apache.sling.cli.impl.junit.LogCapture;
+import org.apache.sling.cli.impl.nexus.RepositoryService;
+import org.apache.sling.testing.mock.osgi.junit.OsgiContext;
+import org.junit.Rule;
+import org.junit.Test;
+import org.mockito.MockedStatic;
+import picocli.CommandLine;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.mockStatic;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.spy;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.verifyNoInteractions;
+import static org.mockito.Mockito.when;
+
+public class CreateJiraVersionCommandTest {
+
+    private static final String VERSION_NAME = "Apache Sling CLI Test 1.0.0";
+
+    @Rule
+    public final OsgiContext osgiContext = new OsgiContext();
+
+    @Rule
+    public final LogCapture logCapture = new 
LogCapture(CreateJiraVersionCommand.class);
+
+    private VersionClient versionClient;
+
+    private void prepare() {
+        versionClient = mock(VersionClient.class);
+        osgiContext.registerService(VersionClient.class, versionClient);
+        osgiContext.registerService(RepositoryService.class, 
mock(RepositoryService.class));
+    }
+
+    @Test
+    public void testDryRunCreatesNothing() throws Exception {
+        prepare();
+        Version version = mock(Version.class);
+        when(version.getName()).thenReturn("1.0.0");
+        when(versionClient.find(any())).thenReturn(version);
+        when(versionClient.findSuccessorVersion(any())).thenReturn(null);
+        when(versionClient.findUnresolvedIssues(any())).thenReturn(List.of());
+
+        Command command = createCommand(ExecutionMode.DRY_RUN, VERSION_NAME);
+        assertEquals(CommandLine.ExitCode.OK, (int) command.call());
+
+        verify(versionClient, never()).create(anyString());
+        assertTrue(logCapture.containsMessage("would be created"));
+    }
+
+    @Test
+    public void testAutoCreatesSuccessor() throws Exception {
+        prepare();
+        Version version = mock(Version.class);
+        when(version.getName()).thenReturn("1.0.0");
+        Version successor = mock(Version.class);
+        when(successor.getName()).thenReturn("1.0.2");
+        when(versionClient.find(any())).thenReturn(version);
+        when(versionClient.findSuccessorVersion(any())).thenReturn(null, 
successor);
+        when(versionClient.findUnresolvedIssues(any())).thenReturn(List.of());
+
+        Command command = createCommand(ExecutionMode.AUTO, VERSION_NAME);
+        assertEquals(CommandLine.ExitCode.OK, (int) command.call());
+
+        verify(versionClient, times(1)).create(anyString());
+    }
+
+    @Test
+    public void testNoRepositoryNoReleaseReturnsUsage() throws Exception {
+        prepare();
+        Command command = createCommandWithoutRepository(ExecutionMode.AUTO);
+        assertEquals(CommandLine.ExitCode.USAGE, (int) command.call());
+        assertTrue(logCapture.containsMessage("Provide either --repository or 
--release."));
+        verifyNoInteractions(versionClient);
+    }
+
+    @Test
+    public void testAutoMovesUnresolvedIssues() throws Exception {
+        prepare();
+        Version version = mock(Version.class);
+        when(version.getName()).thenReturn("1.0.0");
+        Version successor = mock(Version.class);
+        when(successor.getName()).thenReturn("1.0.2");
+        Issue issue = mock(Issue.class);
+        when(issue.getKey()).thenReturn("SLING-123");
+        when(issue.getSummary()).thenReturn("Some bug");
+        when(versionClient.find(any())).thenReturn(version);
+        when(versionClient.findSuccessorVersion(any())).thenReturn(successor);
+        
when(versionClient.findUnresolvedIssues(any())).thenReturn(List.of(issue));
+
+        Command command = createCommand(ExecutionMode.AUTO, VERSION_NAME);
+        assertEquals(CommandLine.ExitCode.OK, (int) command.call());
+
+        verify(versionClient, never()).create(anyString());
+        verify(versionClient, times(1)).moveIssuesToNewVersion(any(), any(), 
any());
+    }
+
+    @Test
+    public void testInteractiveCreatesAndMovesOnYes() throws Exception {
+        prepare();
+        Version version = mock(Version.class);
+        when(version.getName()).thenReturn("1.0.0");
+        Version successor = mock(Version.class);
+        when(successor.getName()).thenReturn("1.0.2");
+        Issue issue = mock(Issue.class);
+        when(issue.getKey()).thenReturn("SLING-123");
+        when(issue.getSummary()).thenReturn("Some bug");
+        when(versionClient.find(any())).thenReturn(version);
+        when(versionClient.findSuccessorVersion(any())).thenReturn(null, 
successor);
+        
when(versionClient.findUnresolvedIssues(any())).thenReturn(List.of(issue));
+
+        try (MockedStatic<UserInput> userInput = mockStatic(UserInput.class)) {
+            userInput.when(() -> UserInput.yesNo(anyString(), 
any())).thenReturn(InputOption.YES);
+            Command command = createCommand(ExecutionMode.INTERACTIVE, 
VERSION_NAME);
+            assertEquals(CommandLine.ExitCode.OK, (int) command.call());
+            verify(versionClient, times(1)).create(anyString());
+            verify(versionClient, times(1)).moveIssuesToNewVersion(any(), 
any(), any());
+        }
+    }
+
+    @Test
+    public void testInteractiveSkipsOnNo() throws Exception {
+        prepare();
+        Version version = mock(Version.class);
+        when(version.getName()).thenReturn("1.0.0");
+        Version successor = mock(Version.class);
+        when(successor.getName()).thenReturn("1.0.2");
+        Issue issue = mock(Issue.class);
+        when(versionClient.find(any())).thenReturn(version);
+        when(versionClient.findSuccessorVersion(any())).thenReturn(successor);
+        
when(versionClient.findUnresolvedIssues(any())).thenReturn(List.of(issue));
+
+        try (MockedStatic<UserInput> userInput = mockStatic(UserInput.class)) {
+            userInput.when(() -> UserInput.yesNo(anyString(), 
any())).thenReturn(InputOption.NO);
+            Command command = createCommand(ExecutionMode.INTERACTIVE, 
VERSION_NAME);
+            assertEquals(CommandLine.ExitCode.OK, (int) command.call());
+            verify(versionClient, never()).moveIssuesToNewVersion(any(), 
any(), any());
+        }
+    }
+
+    @Test
+    public void testIOExceptionReturnsSoftware() throws Exception {
+        // releases() resolves from the staging repository; an IOException 
there surfaces as SOFTWARE
+        versionClient = mock(VersionClient.class);
+        osgiContext.registerService(VersionClient.class, versionClient);
+        RepositoryService repositoryService = mock(RepositoryService.class);
+        when(repositoryService.find(123)).thenThrow(new 
java.io.IOException("nexus down"));
+        osgiContext.registerService(RepositoryService.class, 
repositoryService);
+
+        Command command = createCommand(ExecutionMode.AUTO, null);
+        assertEquals(CommandLine.ExitCode.SOFTWARE, (int) command.call());
+        assertTrue(logCapture.containsMessage("Failed executing command"));
+    }
+
+    private Command createCommandWithoutRepository(ExecutionMode 
executionMode) throws IllegalAccessException {
+        CreateJiraVersionCommand createJiraVersionCommand = spy(new 
CreateJiraVersionCommand());
+        ReusableCLIOptions reusableCLIOptions = mock(ReusableCLIOptions.class);
+        FieldUtils.writeField(reusableCLIOptions, "executionMode", 
executionMode, true);
+        FieldUtils.writeField(createJiraVersionCommand, "reusableCLIOptions", 
reusableCLIOptions, true);
+        osgiContext.registerInjectActivateService(createJiraVersionCommand);
+        Command result = osgiContext.getService(Command.class);
+        assertTrue(
+                "Expected to retrieve the CreateJiraVersionCommand from the 
mocked OSGi environment.",
+                result instanceof CreateJiraVersionCommand);
+        return result;
+    }
+
+    private Command createCommand(ExecutionMode executionMode, String 
jiraVersionName) throws IllegalAccessException {
+        CreateJiraVersionCommand createJiraVersionCommand = spy(new 
CreateJiraVersionCommand());
+        FieldUtils.writeField(createJiraVersionCommand, "repositoryId", 123, 
true);
+        FieldUtils.writeField(createJiraVersionCommand, "jiraVersionName", 
jiraVersionName, true);
+        ReusableCLIOptions reusableCLIOptions = mock(ReusableCLIOptions.class);
+        FieldUtils.writeField(reusableCLIOptions, "executionMode", 
executionMode, true);
+        FieldUtils.writeField(createJiraVersionCommand, "reusableCLIOptions", 
reusableCLIOptions, true);
+        osgiContext.registerInjectActivateService(createJiraVersionCommand);
+        Command result = osgiContext.getService(Command.class);
+        assertTrue(
+                "Expected to retrieve the CreateJiraVersionCommand from the 
mocked OSGi environment.",
+                result instanceof CreateJiraVersionCommand);
+        return result;
+    }
+}
diff --git 
a/src/test/java/org/apache/sling/cli/impl/release/ReleaseJiraVersionCommandTest.java
 
b/src/test/java/org/apache/sling/cli/impl/release/ReleaseJiraVersionCommandTest.java
new file mode 100644
index 0000000..d7715da
--- /dev/null
+++ 
b/src/test/java/org/apache/sling/cli/impl/release/ReleaseJiraVersionCommandTest.java
@@ -0,0 +1,152 @@
+/*
+ * 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.commons.lang3.reflect.FieldUtils;
+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.junit.LogCapture;
+import org.apache.sling.cli.impl.nexus.RepositoryService;
+import org.apache.sling.cli.impl.nexus.StagingRepository;
+import org.apache.sling.testing.mock.osgi.junit.OsgiContext;
+import org.junit.Rule;
+import org.junit.Test;
+import org.mockito.MockedStatic;
+import picocli.CommandLine;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.mockStatic;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.spy;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+public class ReleaseJiraVersionCommandTest {
+
+    @Rule
+    public final OsgiContext osgiContext = new OsgiContext();
+
+    @Rule
+    public final LogCapture logCapture = new 
LogCapture(ReleaseJiraVersionCommand.class);
+
+    private RepositoryService repositoryService;
+    private VersionClient versionClient;
+
+    private void prepare() throws Exception {
+        StagingRepository stagingRepository = mock(StagingRepository.class);
+        repositoryService = mock(RepositoryService.class);
+        when(repositoryService.find(123)).thenReturn(stagingRepository);
+        Set<Release> releases =
+                Set.of(Release.fromString("Apache Sling CLI Test 
1.0.0").get(0));
+        
when(repositoryService.getReleases(stagingRepository)).thenReturn(releases);
+
+        Issue issue = mock(Issue.class);
+        when(issue.getKey()).thenReturn("SLING-1");
+        when(issue.getSummary()).thenReturn("A fixed issue");
+        when(issue.getStatus()).thenReturn("Closed");
+        when(issue.getResolution()).thenReturn("Fixed");
+
+        versionClient = mock(VersionClient.class);
+        when(versionClient.findFixedIssues(any())).thenReturn(List.of(issue));
+
+        osgiContext.registerService(RepositoryService.class, 
repositoryService);
+        osgiContext.registerService(VersionClient.class, versionClient);
+    }
+
+    @Test
+    public void testDryRun() throws Exception {
+        prepare();
+        Command command = createCommand(123, ExecutionMode.DRY_RUN);
+        assertEquals(CommandLine.ExitCode.OK, (int) command.call());
+        verify(versionClient, never()).release(any());
+        assertTrue(logCapture.containsMessage("The following Jira versions 
would be released:"));
+    }
+
+    @Test
+    public void testInteractiveYes() throws Exception {
+        prepare();
+        try (MockedStatic<UserInput> userInputMock = 
mockStatic(UserInput.class)) {
+            userInputMock
+                    .when(() -> UserInput.yesNo(anyString(), 
any(InputOption.class)))
+                    .thenReturn(InputOption.YES);
+            Command command = createCommand(123, ExecutionMode.INTERACTIVE);
+            assertEquals(CommandLine.ExitCode.OK, (int) command.call());
+            verify(versionClient, times(1)).release(any());
+        }
+    }
+
+    @Test
+    public void testAuto() throws Exception {
+        prepare();
+        Command command = createCommand(123, ExecutionMode.AUTO);
+        assertEquals(CommandLine.ExitCode.OK, (int) command.call());
+        verify(versionClient, times(1)).release(any());
+    }
+
+    @Test
+    public void testReleaseByName() throws Exception {
+        // the release is resolved from --release, so the command works 
without the (now-gone) staging repo
+        prepare();
+        Command command = createCommandByName("Apache Sling CLI Test 1.0.0", 
ExecutionMode.AUTO);
+        assertEquals(CommandLine.ExitCode.OK, (int) command.call());
+        verify(versionClient, times(1)).release(any());
+        verify(repositoryService, never()).find(anyInt());
+    }
+
+    private Command createCommand(int repositoryId, ExecutionMode 
executionMode) throws Exception {
+        ReleaseJiraVersionCommand releaseJiraVersionCommand = spy(new 
ReleaseJiraVersionCommand());
+        FieldUtils.writeField(releaseJiraVersionCommand, "repositoryId", 
repositoryId, true);
+        ReusableCLIOptions reusableCLIOptions = mock(ReusableCLIOptions.class);
+        FieldUtils.writeField(reusableCLIOptions, "executionMode", 
executionMode, true);
+        FieldUtils.writeField(releaseJiraVersionCommand, "reusableCLIOptions", 
reusableCLIOptions, true);
+        osgiContext.registerInjectActivateService(releaseJiraVersionCommand);
+        Command result = osgiContext.getService(Command.class);
+        assertTrue(
+                "Expected to retrieve the ReleaseJiraVersionCommand from the 
mocked OSGi environment.",
+                result instanceof ReleaseJiraVersionCommand);
+        return result;
+    }
+
+    private Command createCommandByName(String releaseName, ExecutionMode 
executionMode) throws Exception {
+        ReleaseJiraVersionCommand releaseJiraVersionCommand = spy(new 
ReleaseJiraVersionCommand());
+        FieldUtils.writeField(releaseJiraVersionCommand, "releaseName", 
releaseName, true);
+        ReusableCLIOptions reusableCLIOptions = mock(ReusableCLIOptions.class);
+        FieldUtils.writeField(reusableCLIOptions, "executionMode", 
executionMode, true);
+        FieldUtils.writeField(releaseJiraVersionCommand, "reusableCLIOptions", 
reusableCLIOptions, true);
+        osgiContext.registerInjectActivateService(releaseJiraVersionCommand);
+        Command result = osgiContext.getService(Command.class);
+        assertTrue(
+                "Expected to retrieve the ReleaseJiraVersionCommand from the 
mocked OSGi environment.",
+                result instanceof ReleaseJiraVersionCommand);
+        return result;
+    }
+}
diff --git 
a/src/test/java/org/apache/sling/cli/impl/release/UpdateLocalSiteCommandTest.java
 
b/src/test/java/org/apache/sling/cli/impl/release/UpdateLocalSiteCommandTest.java
new file mode 100644
index 0000000..f034fb6
--- /dev/null
+++ 
b/src/test/java/org/apache/sling/cli/impl/release/UpdateLocalSiteCommandTest.java
@@ -0,0 +1,154 @@
+/*
+ * 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.io.IOException;
+import java.util.Set;
+
+import org.apache.commons.lang3.reflect.FieldUtils;
+import org.apache.sling.cli.impl.Command;
+import org.apache.sling.cli.impl.jbake.JBakeContentUpdater;
+import org.apache.sling.cli.impl.junit.LogCapture;
+import org.apache.sling.cli.impl.nexus.RepositoryService;
+import org.apache.sling.cli.impl.nexus.StagingRepository;
+import org.apache.sling.testing.mock.osgi.junit.OsgiContext;
+import org.eclipse.jgit.api.CloneCommand;
+import org.eclipse.jgit.api.DiffCommand;
+import org.eclipse.jgit.api.Git;
+import org.eclipse.jgit.api.ResetCommand;
+import org.junit.Rule;
+import org.junit.Test;
+import org.mockito.MockedConstruction;
+import org.mockito.MockedStatic;
+import picocli.CommandLine;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.atLeastOnce;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.mockConstruction;
+import static org.mockito.Mockito.mockStatic;
+import static org.mockito.Mockito.spy;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+public class UpdateLocalSiteCommandTest {
+
+    @Rule
+    public final OsgiContext osgiContext = new OsgiContext();
+
+    @Rule
+    public final LogCapture logCapture = new 
LogCapture(UpdateLocalSiteCommand.class);
+
+    /**
+     * Stubs out the JGit interactions so that no repository is cloned, opened 
or reset against the
+     * real filesystem or network. The {@code Git} instance returned by {@code 
Git.open(...)} is a
+     * mock whose {@code diff()} returns a mock {@link DiffCommand}.
+     */
+    private MockedStatic<Git> stubGit() {
+        MockedStatic<Git> git = mockStatic(Git.class);
+        Git gitInstance = mock(Git.class);
+        // ensureRepo: the checkout already exists -> 
Git.open(...).reset()...call()
+        ResetCommand resetCommand = mock(ResetCommand.class);
+        when(resetCommand.setMode(any())).thenReturn(resetCommand);
+        when(gitInstance.reset()).thenReturn(resetCommand);
+        // call(): git.diff().setOutputStream(...).call()
+        DiffCommand diffCommand = mock(DiffCommand.class);
+        when(diffCommand.setOutputStream(any())).thenReturn(diffCommand);
+        when(gitInstance.diff()).thenReturn(diffCommand);
+        git.when(() -> Git.open(any())).thenReturn(gitInstance);
+        // ensureRepo: when the checkout does not yet exist, it is cloned 
instead
+        CloneCommand cloneCommand = mock(CloneCommand.class);
+        when(cloneCommand.setURI(any())).thenReturn(cloneCommand);
+        when(cloneCommand.setProgressMonitor(any())).thenReturn(cloneCommand);
+        when(cloneCommand.setDirectory(any())).thenReturn(cloneCommand);
+        git.when(Git::cloneRepository).thenReturn(cloneCommand);
+        return git;
+    }
+
+    @Test
+    public void testNoRepositoryNoReleaseReturnsUsage() throws Exception {
+        osgiContext.registerService(RepositoryService.class, 
mock(RepositoryService.class));
+        try (MockedStatic<Git> git = stubGit()) {
+            Command command = createCommand(null, null);
+            assertEquals(CommandLine.ExitCode.USAGE, (int) command.call());
+            assertTrue(logCapture.containsMessage("Provide either --repository 
or --release."));
+        }
+    }
+
+    @Test
+    public void testReleaseNameUpdatesContent() throws Exception {
+        osgiContext.registerService(RepositoryService.class, 
mock(RepositoryService.class));
+        try (MockedStatic<Git> git = stubGit();
+                MockedConstruction<JBakeContentUpdater> updater = 
mockConstruction(JBakeContentUpdater.class)) {
+            Command command = createCommand(null, "Apache Sling Foo 1.2.0");
+            assertEquals(CommandLine.ExitCode.OK, (int) command.call());
+
+            JBakeContentUpdater instance = updater.constructed().get(0);
+            verify(instance).updateDownloads(any(), eq("Foo"), eq("1.2.0"));
+            verify(instance).updateReleases(any(), eq("Foo"), eq("1.2.0"), 
any());
+        }
+    }
+
+    @Test
+    public void testRepositoryResolvesReleasesFromService() throws Exception {
+        RepositoryService repositoryService = mock(RepositoryService.class);
+        StagingRepository repository = mock(StagingRepository.class);
+        when(repositoryService.find(123)).thenReturn(repository);
+        when(repositoryService.getReleases(repository))
+                .thenReturn(Set.copyOf(Release.fromString("Apache Sling Bar 
2.0.0")));
+        osgiContext.registerService(RepositoryService.class, 
repositoryService);
+
+        try (MockedStatic<Git> git = stubGit();
+                MockedConstruction<JBakeContentUpdater> updater = 
mockConstruction(JBakeContentUpdater.class)) {
+            Command command = createCommand(123, null);
+            assertEquals(CommandLine.ExitCode.OK, (int) command.call());
+
+            JBakeContentUpdater instance = updater.constructed().get(0);
+            verify(instance, atLeastOnce()).updateDownloads(any(), eq("Bar"), 
eq("2.0.0"));
+        }
+    }
+
+    @Test
+    public void testIOExceptionReturnsSoftware() throws Exception {
+        RepositoryService repositoryService = mock(RepositoryService.class);
+        when(repositoryService.find(123)).thenThrow(new IOException("nexus 
down"));
+        osgiContext.registerService(RepositoryService.class, 
repositoryService);
+
+        try (MockedStatic<Git> git = stubGit()) {
+            Command command = createCommand(123, null);
+            assertEquals(CommandLine.ExitCode.SOFTWARE, (int) command.call());
+            assertTrue(logCapture.containsMessage("Failed executing command"));
+        }
+    }
+
+    private Command createCommand(Integer repositoryId, String releaseName) 
throws IllegalAccessException {
+        UpdateLocalSiteCommand updateLocalSiteCommand = spy(new 
UpdateLocalSiteCommand());
+        FieldUtils.writeField(updateLocalSiteCommand, "repositoryId", 
repositoryId, true);
+        FieldUtils.writeField(updateLocalSiteCommand, "releaseName", 
releaseName, true);
+        osgiContext.registerInjectActivateService(updateLocalSiteCommand);
+        Command result = osgiContext.getService(Command.class);
+        assertTrue(
+                "Expected to retrieve the UpdateLocalSiteCommand from the 
mocked OSGi environment.",
+                result instanceof UpdateLocalSiteCommand);
+        return result;
+    }
+}
diff --git 
a/src/test/java/org/apache/sling/cli/impl/release/UpdateReporterCommandTest.java
 
b/src/test/java/org/apache/sling/cli/impl/release/UpdateReporterCommandTest.java
index 10f53fa..fba8cc5 100644
--- 
a/src/test/java/org/apache/sling/cli/impl/release/UpdateReporterCommandTest.java
+++ 
b/src/test/java/org/apache/sling/cli/impl/release/UpdateReporterCommandTest.java
@@ -118,6 +118,43 @@ public class UpdateReporterCommandTest {
         verify(client, times(2)).execute(any());
     }
 
+    @Test
+    public void testAutoByName() throws Exception {
+        // resolves the release from --release rather than a (possibly gone) 
staging repository
+        Command updateReporter = createCommandByName("Apache Sling CLI 1", 
ExecutionMode.AUTO);
+        CloseableHttpResponse response = mock(CloseableHttpResponse.class);
+        StatusLine statusLine = mock(StatusLine.class);
+        when(response.getStatusLine()).thenReturn(statusLine);
+        when(statusLine.getStatusCode()).thenReturn(200);
+        when(client.execute(any())).thenReturn(response);
+        assertEquals(0, (int) updateReporter.call());
+        verify(client, times(1)).execute(any());
+    }
+
+    @Test
+    public void testNoRepositoryNoReleaseReturnsUsage() throws Exception {
+        UpdateReporterCommand updateReporterCommand = spy(new 
UpdateReporterCommand());
+        ReusableCLIOptions reusableCLIOptions = mock(ReusableCLIOptions.class);
+        FieldUtils.writeField(reusableCLIOptions, "executionMode", 
ExecutionMode.AUTO, true);
+        FieldUtils.writeField(updateReporterCommand, "reusableCLIOptions", 
reusableCLIOptions, true);
+        osgiContext.registerInjectActivateService(updateReporterCommand);
+        Command command = osgiContext.getService(Command.class);
+        assertEquals(2, (int) command.call());
+        assertTrue(logCapture.containsMessage("Provide either --repository or 
--release."));
+        verifyNoInteractions(client);
+    }
+
+    @Test
+    public void testReporterFailureReturnsSoftware() throws Exception {
+        Command updateReporter = createCommand(42, ExecutionMode.AUTO);
+        CloseableHttpResponse response = mock(CloseableHttpResponse.class);
+        StatusLine statusLine = mock(StatusLine.class);
+        when(response.getStatusLine()).thenReturn(statusLine);
+        when(statusLine.getStatusCode()).thenReturn(500);
+        when(client.execute(any())).thenReturn(response);
+        assertEquals(1, (int) updateReporter.call());
+    }
+
     private Command createCommand(int repositoryId, ExecutionMode 
executionMode) throws IllegalAccessException {
         UpdateReporterCommand updateReporterCommand = spy(new 
UpdateReporterCommand());
         FieldUtils.writeField(updateReporterCommand, "repositoryId", 
repositoryId, true);
@@ -131,4 +168,18 @@ public class UpdateReporterCommandTest {
                 result instanceof UpdateReporterCommand);
         return result;
     }
+
+    private Command createCommandByName(String releaseName, ExecutionMode 
executionMode) throws IllegalAccessException {
+        UpdateReporterCommand updateReporterCommand = spy(new 
UpdateReporterCommand());
+        FieldUtils.writeField(updateReporterCommand, "releaseName", 
releaseName, true);
+        ReusableCLIOptions reusableCLIOptions = mock(ReusableCLIOptions.class);
+        FieldUtils.writeField(reusableCLIOptions, "executionMode", 
executionMode, true);
+        FieldUtils.writeField(updateReporterCommand, "reusableCLIOptions", 
reusableCLIOptions, true);
+        osgiContext.registerInjectActivateService(updateReporterCommand);
+        Command result = osgiContext.getService(Command.class);
+        assertTrue(
+                "Expected to retrieve the UpdateReporterCommand from the 
mocked OSGi environment.",
+                result instanceof UpdateReporterCommand);
+        return result;
+    }
 }
diff --git 
a/src/test/java/org/apache/sling/cli/impl/release/VerifyReleasesCommandTest.java
 
b/src/test/java/org/apache/sling/cli/impl/release/VerifyReleasesCommandTest.java
new file mode 100644
index 0000000..e7f606d
--- /dev/null
+++ 
b/src/test/java/org/apache/sling/cli/impl/release/VerifyReleasesCommandTest.java
@@ -0,0 +1,127 @@
+/*
+ * 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.nio.file.Path;
+import java.nio.file.Paths;
+import java.util.Set;
+
+import org.apache.commons.lang3.reflect.FieldUtils;
+import org.apache.sling.cli.impl.Command;
+import org.apache.sling.cli.impl.ExecutionMode;
+import org.apache.sling.cli.impl.ci.CIStatusValidator;
+import org.apache.sling.cli.impl.junit.LogCapture;
+import org.apache.sling.cli.impl.nexus.Artifact;
+import org.apache.sling.cli.impl.nexus.LocalRepository;
+import org.apache.sling.cli.impl.nexus.RepositoryService;
+import org.apache.sling.cli.impl.nexus.StagingRepository;
+import org.apache.sling.cli.impl.pgp.HashValidator;
+import org.apache.sling.cli.impl.pgp.PGPSignatureValidator;
+import org.apache.sling.testing.mock.osgi.junit.OsgiContext;
+import org.junit.Rule;
+import org.junit.Test;
+import picocli.CommandLine;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.spy;
+import static org.mockito.Mockito.when;
+
+public class VerifyReleasesCommandTest {
+
+    @Rule
+    public final OsgiContext osgiContext = new OsgiContext();
+
+    @Rule
+    public final LogCapture logCapture = new 
LogCapture(VerifyReleasesCommand.class);
+
+    @Test
+    public void testEmptyRepositoryIsValid() throws Exception {
+        // A downloaded repository with no artifacts runs no checks and is 
reported valid.
+        LocalRepository localRepository = mock(LocalRepository.class);
+        when(localRepository.getArtifacts()).thenReturn(Set.<Artifact>of());
+        when(localRepository.getRootFolder()).thenReturn(Paths.get("/tmp"));
+
+        RepositoryService repositoryService = mock(RepositoryService.class);
+        StagingRepository stagingRepository = mock(StagingRepository.class);
+        when(repositoryService.find(123)).thenReturn(stagingRepository);
+        
when(repositoryService.download(stagingRepository)).thenReturn(localRepository);
+
+        osgiContext.registerService(RepositoryService.class, 
repositoryService);
+        osgiContext.registerService(PGPSignatureValidator.class, 
mock(PGPSignatureValidator.class));
+        osgiContext.registerService(CIStatusValidator.class, 
mock(CIStatusValidator.class));
+        osgiContext.registerService(HashValidator.class, 
mock(HashValidator.class));
+
+        Command command = createCommand(123, ExecutionMode.DRY_RUN);
+        assertEquals(CommandLine.ExitCode.OK, (int) command.call());
+        assertTrue(logCapture.containsMessage("VALID (0 checks executed)"));
+    }
+
+    @Test
+    public void testInvalidSignatureFailsVerification() throws Exception {
+        // One artifact whose signature, SHA-1 and MD5 all fail -> command 
reports INVALID.
+        StagingRepository stagingRepository = mock(StagingRepository.class);
+        Artifact jar =
+                new Artifact(stagingRepository, "org.apache.sling", 
"org.apache.sling.cli.test", "1.0.0", null, "jar");
+        LocalRepository localRepository = mock(LocalRepository.class);
+        when(localRepository.getArtifacts()).thenReturn(Set.of(jar));
+        when(localRepository.getRootFolder()).thenReturn(Paths.get("/tmp"));
+
+        RepositoryService repositoryService = mock(RepositoryService.class);
+        when(repositoryService.find(123)).thenReturn(stagingRepository);
+        
when(repositoryService.download(stagingRepository)).thenReturn(localRepository);
+
+        PGPSignatureValidator pgp = mock(PGPSignatureValidator.class);
+        PGPSignatureValidator.ValidationResult pgpResult = 
mock(PGPSignatureValidator.ValidationResult.class);
+        when(pgpResult.isValid()).thenReturn(false);
+        when(pgp.verify(any(Path.class), 
any(Path.class))).thenReturn(pgpResult);
+
+        HashValidator hashValidator = mock(HashValidator.class);
+        HashValidator.ValidationResult hashResult = 
mock(HashValidator.ValidationResult.class);
+        when(hashResult.isValid()).thenReturn(false);
+        when(hashResult.getExpectedHash()).thenReturn("expected");
+        when(hashResult.getActualHash()).thenReturn("actual");
+        when(hashValidator.validate(any(Path.class), any(Path.class), 
any())).thenReturn(hashResult);
+
+        osgiContext.registerService(RepositoryService.class, 
repositoryService);
+        osgiContext.registerService(PGPSignatureValidator.class, pgp);
+        osgiContext.registerService(CIStatusValidator.class, 
mock(CIStatusValidator.class));
+        osgiContext.registerService(HashValidator.class, hashValidator);
+
+        Command command = createCommand(123, ExecutionMode.DRY_RUN);
+        assertEquals(CommandLine.ExitCode.USAGE, (int) command.call());
+        assertTrue(logCapture.containsMessage("INVALID"));
+    }
+
+    private Command createCommand(int repositoryId, ExecutionMode 
executionMode) throws IllegalAccessException {
+        VerifyReleasesCommand verifyReleasesCommand = spy(new 
VerifyReleasesCommand());
+        FieldUtils.writeField(verifyReleasesCommand, "repositoryId", 
repositoryId, true);
+        ReusableCLIOptions reusableCLIOptions = mock(ReusableCLIOptions.class);
+        FieldUtils.writeField(reusableCLIOptions, "executionMode", 
executionMode, true);
+        FieldUtils.writeField(verifyReleasesCommand, "reusableCLIOptions", 
reusableCLIOptions, true);
+        osgiContext.registerInjectActivateService(verifyReleasesCommand);
+        Command result = osgiContext.getService(Command.class);
+        assertTrue(
+                "Expected to retrieve the VerifyReleasesCommand from the 
mocked OSGi environment.",
+                result instanceof VerifyReleasesCommand);
+        return result;
+    }
+}

Reply via email to