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; + } +}
