You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@sling.apache.org by ro...@apache.org on 2026/07/04 13:03:07 UTC
(sling-org-apache-sling-committer-cli) 01/01: SLING-13253 - release close-staging command
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 <ro...@apache.org>
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
+ })
+@CommandLine.Command(
+ 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;
+ }
+}