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

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

commit da20a33a5e911a2cf90cd1ce57251dc382a28220
Author: Roy Teeuwen <[email protected]>
AuthorDate: Sat Jul 4 15:02:56 2026 +0200

    SLING-13253 - release close-staging command
    
    Adds 'release close-staging' to close an open Nexus staging repository after
    'mvn release:perform', making it ready for verification and voting.
    
    Part of splitting PR #28 into per-command changes; builds on the list PR.
---
 .../cli/impl/release/CloseStagingCommand.java      | 117 +++++++++++++++++++++
 .../cli/impl/release/CloseStagingCommandTest.java  | 116 ++++++++++++++++++++
 2 files changed, 233 insertions(+)

diff --git 
a/src/main/java/org/apache/sling/cli/impl/release/CloseStagingCommand.java 
b/src/main/java/org/apache/sling/cli/impl/release/CloseStagingCommand.java
new file mode 100644
index 0000000..5bd38f1
--- /dev/null
+++ b/src/main/java/org/apache/sling/cli/impl/release/CloseStagingCommand.java
@@ -0,0 +1,117 @@
+/*
+ * 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.stream.Collectors;
+
+import org.apache.sling.cli.impl.Command;
+import org.apache.sling.cli.impl.InputOption;
+import org.apache.sling.cli.impl.UserInput;
+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 + "=" + 
CloseStagingCommand.GROUP,
+            Command.PROPERTY_NAME_COMMAND_NAME + "=" + CloseStagingCommand.NAME
+        })
[email protected](
+        name = CloseStagingCommand.NAME,
+        description =
+                "Closes an open Nexus staging repository after 'mvn 
release:perform', making it ready for verification and voting",
+        subcommands = CommandLine.HelpCommand.class)
+public class CloseStagingCommand implements Command {
+
+    static final String GROUP = "release";
+    static final String NAME = "close-staging";
+
+    private static final Logger LOGGER = 
LoggerFactory.getLogger(CloseStagingCommand.class);
+
+    @CommandLine.Option(
+            names = {"-r", "--repository"},
+            description = "Nexus staging repository id (numeric part, e.g. 
1087)",
+            required = true)
+    private Integer repositoryId;
+
+    @CommandLine.Mixin
+    private ReusableCLIOptions reusableCLIOptions;
+
+    @Reference
+    private RepositoryService repositoryService;
+
+    @Override
+    public Integer call() {
+        try {
+            StagingRepository repository = 
repositoryService.findAny(repositoryId);
+            // Derive the description from the staged artifacts' POM (name + 
version), since
+            // releases staged via the plain maven-deploy-plugin only get the 
generic Nexus
+            // "Implicitly created (auto staging)" description. The repository 
is still open at
+            // this point, so it is not in the Lucene index yet; browse its 
content directly.
+            String description = 
repositoryService.getReleasesFromContent(repository).stream()
+                    .map(Release::getFullName)
+                    .collect(Collectors.joining(", "));
+            if (description.isEmpty()) {
+                description = repository.getDescription();
+            }
+            switch (reusableCLIOptions.executionMode) {
+                case DRY_RUN:
+                    LOGGER.info(
+                            "Would close staging repository {} with 
description \"{}\".",
+                            repository.getRepositoryId(),
+                            description);
+                    break;
+                case INTERACTIVE:
+                    InputOption answer = UserInput.yesNo(
+                            String.format(
+                                    "Close staging repository %s with 
description \"%s\"?",
+                                    repository.getRepositoryId(), description),
+                            InputOption.YES);
+                    if (InputOption.YES.equals(answer)) {
+                        doClose(repository, description);
+                    } else {
+                        LOGGER.info("Aborted.");
+                    }
+                    break;
+                case AUTO:
+                    doClose(repository, description);
+                    break;
+            }
+        } catch (IOException e) {
+            LOGGER.warn("Failed executing command", e);
+            return CommandLine.ExitCode.SOFTWARE;
+        }
+        return CommandLine.ExitCode.OK;
+    }
+
+    private void doClose(StagingRepository repository, String description) 
throws IOException {
+        LOGGER.info(
+                "Closing staging repository {} with description \"{}\"...", 
repository.getRepositoryId(), description);
+        repositoryService.close(repository, description);
+        LOGGER.info(
+                "Done. Repository {} is now closed and ready for verification 
and voting.",
+                repository.getRepositoryId());
+    }
+}
diff --git 
a/src/test/java/org/apache/sling/cli/impl/release/CloseStagingCommandTest.java 
b/src/test/java/org/apache/sling/cli/impl/release/CloseStagingCommandTest.java
new file mode 100644
index 0000000..638a4e2
--- /dev/null
+++ 
b/src/test/java/org/apache/sling/cli/impl/release/CloseStagingCommandTest.java
@@ -0,0 +1,116 @@
+/*
+ * 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.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.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.Before;
+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.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 CloseStagingCommandTest {
+
+    @Rule
+    public final OsgiContext osgiContext = new OsgiContext();
+
+    @Rule
+    public final LogCapture logCapture = new 
LogCapture(CloseStagingCommand.class);
+
+    private RepositoryService repositoryService;
+    private StagingRepository stagingRepository;
+
+    @Before
+    public void before() throws Exception {
+        stagingRepository = mock(StagingRepository.class);
+        
when(stagingRepository.getRepositoryId()).thenReturn("orgapachesling-123");
+        when(stagingRepository.getDescription()).thenReturn("Apache Sling CLI 
Test 1.0.0");
+
+        repositoryService = mock(RepositoryService.class);
+        when(repositoryService.findAny(123)).thenReturn(stagingRepository);
+        Set<Release> releases =
+                Set.of(Release.fromString("Apache Sling CLI Test 
1.0.0").get(0));
+        
when(repositoryService.getReleasesFromContent(stagingRepository)).thenReturn(releases);
+
+        osgiContext.registerService(RepositoryService.class, 
repositoryService);
+    }
+
+    @Test
+    public void testDryRun() throws Exception {
+        Command command = createCommand(123, ExecutionMode.DRY_RUN);
+        assertEquals(CommandLine.ExitCode.OK, (int) command.call());
+        assertTrue(logCapture.containsMessage(
+                "Would close staging repository orgapachesling-123 with 
description \"Apache Sling CLI Test 1.0.0\"."));
+        verify(repositoryService, never()).close(any(), any());
+    }
+
+    @Test
+    public void testInteractiveYes() throws Exception {
+        try (MockedStatic<UserInput> userInputMock = 
mockStatic(UserInput.class)) {
+            String question =
+                    "Close staging repository orgapachesling-123 with 
description \"Apache Sling CLI Test 1.0.0\"?";
+            userInputMock.when(() -> UserInput.yesNo(question, 
InputOption.YES)).thenReturn(InputOption.YES);
+            Command command = createCommand(123, ExecutionMode.INTERACTIVE);
+            assertEquals(CommandLine.ExitCode.OK, (int) command.call());
+            verify(repositoryService).close(stagingRepository, "Apache Sling 
CLI Test 1.0.0");
+        }
+    }
+
+    @Test
+    public void testAuto() throws Exception {
+        Command command = createCommand(123, ExecutionMode.AUTO);
+        assertEquals(CommandLine.ExitCode.OK, (int) command.call());
+        verify(repositoryService, times(1)).close(stagingRepository, "Apache 
Sling CLI Test 1.0.0");
+    }
+
+    private Command createCommand(int repositoryId, ExecutionMode 
executionMode) throws IllegalAccessException {
+        CloseStagingCommand closeStagingCommand = spy(new 
CloseStagingCommand());
+        FieldUtils.writeField(closeStagingCommand, "repositoryId", 
repositoryId, true);
+        ReusableCLIOptions reusableCLIOptions = mock(ReusableCLIOptions.class);
+        FieldUtils.writeField(reusableCLIOptions, "executionMode", 
executionMode, true);
+        FieldUtils.writeField(closeStagingCommand, "reusableCLIOptions", 
reusableCLIOptions, true);
+        osgiContext.registerInjectActivateService(closeStagingCommand);
+        Command result = osgiContext.getService(Command.class);
+        assertTrue(
+                "Expected to retrieve the CloseStagingCommand from the mocked 
OSGi environment.",
+                result instanceof CloseStagingCommand);
+        return result;
+    }
+}

Reply via email to