This is an automated email from the ASF dual-hosted git repository. royteeuwen pushed a commit to branch feature/SLING-13253-promote-drop in repository https://gitbox.apache.org/repos/asf/sling-org-apache-sling-committer-cli.git
commit 8f3503f5d1188c43ebf5fe5bb32e5d4fca5edb07 Author: Roy Teeuwen <[email protected]> AuthorDate: Sat Jul 4 15:03:23 2026 +0200 SLING-13253 - release promote and drop staging repository commands Adds 'release promote' (promote a closed Nexus staging repository to Maven Central after a successful vote) and 'release drop' (drop a staging repository when a vote fails or to clean up). Part of splitting PR #28 into per-command changes; builds on the list PR. --- .../apache/sling/cli/impl/release/DropCommand.java | 102 ++++++++++++++++++ .../sling/cli/impl/release/PromoteCommand.java | 103 ++++++++++++++++++ .../sling/cli/impl/release/DropCommandTest.java | 120 +++++++++++++++++++++ .../sling/cli/impl/release/PromoteCommandTest.java | 113 +++++++++++++++++++ 4 files changed, 438 insertions(+) diff --git a/src/main/java/org/apache/sling/cli/impl/release/DropCommand.java b/src/main/java/org/apache/sling/cli/impl/release/DropCommand.java new file mode 100644 index 0000000..fe6675d --- /dev/null +++ b/src/main/java/org/apache/sling/cli/impl/release/DropCommand.java @@ -0,0 +1,102 @@ +/* + * 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 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 + "=" + DropCommand.GROUP, + Command.PROPERTY_NAME_COMMAND_NAME + "=" + DropCommand.NAME + }) [email protected]( + name = DropCommand.NAME, + description = "Drops a Nexus staging repository. Use when a vote fails or to clean up a failed release.", + subcommands = CommandLine.HelpCommand.class) +public class DropCommand implements Command { + + static final String GROUP = "release"; + static final String NAME = "drop"; + + private static final Logger LOGGER = LoggerFactory.getLogger(DropCommand.class); + + @CommandLine.Option( + names = {"-r", "--repository"}, + description = "Nexus staging repository id", + required = true) + private Integer repositoryId; + + @CommandLine.Mixin + private ReusableCLIOptions reusableCLIOptions; + + @Reference + private RepositoryService repositoryService; + + @Override + public Integer call() { + try { + StagingRepository repository = repositoryService.findAny(repositoryId); + switch (reusableCLIOptions.executionMode) { + case DRY_RUN: + LOGGER.info( + "Would drop staging repository {}: {}", + repository.getRepositoryId(), + repository.getDescription()); + break; + case INTERACTIVE: + InputOption answer = UserInput.yesNo( + String.format( + "Drop staging repository %s (%s)? This cannot be undone.", + repository.getRepositoryId(), repository.getDescription()), + InputOption.NO); + if (InputOption.YES.equals(answer)) { + doDrop(repository); + } else { + LOGGER.info("Aborted."); + } + break; + case AUTO: + doDrop(repository); + break; + } + } catch (IOException e) { + LOGGER.warn("Failed executing command", e); + return CommandLine.ExitCode.SOFTWARE; + } + return CommandLine.ExitCode.OK; + } + + private void doDrop(StagingRepository repository) throws IOException { + LOGGER.info("Dropping staging repository {}...", repository.getRepositoryId()); + repositoryService.drop(repository); + LOGGER.info("Done. Repository {} has been dropped.", repository.getRepositoryId()); + } +} diff --git a/src/main/java/org/apache/sling/cli/impl/release/PromoteCommand.java b/src/main/java/org/apache/sling/cli/impl/release/PromoteCommand.java new file mode 100644 index 0000000..1a69245 --- /dev/null +++ b/src/main/java/org/apache/sling/cli/impl/release/PromoteCommand.java @@ -0,0 +1,103 @@ +/* + * 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 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 + "=" + PromoteCommand.GROUP, + Command.PROPERTY_NAME_COMMAND_NAME + "=" + PromoteCommand.NAME + }) [email protected]( + name = PromoteCommand.NAME, + description = "Promotes a closed Nexus staging repository to Maven Central after a successful vote", + subcommands = CommandLine.HelpCommand.class) +public class PromoteCommand implements Command { + + static final String GROUP = "release"; + static final String NAME = "promote"; + + private static final Logger LOGGER = LoggerFactory.getLogger(PromoteCommand.class); + + @CommandLine.Option( + names = {"-r", "--repository"}, + description = "Nexus staging repository id", + required = true) + private Integer repositoryId; + + @CommandLine.Mixin + private ReusableCLIOptions reusableCLIOptions; + + @Reference + private RepositoryService repositoryService; + + @Override + public Integer call() { + try { + StagingRepository repository = repositoryService.find(repositoryId); + Set<Release> releases = repositoryService.getReleases(repository); + String releaseNames = releases.stream().map(Release::getFullName).collect(Collectors.joining(", ")); + switch (reusableCLIOptions.executionMode) { + case DRY_RUN: + LOGGER.info( + "Would promote {} to Maven Central from repository {}", + releaseNames, + repository.getRepositoryId()); + break; + case INTERACTIVE: + InputOption answer = UserInput.yesNo( + String.format("Promote %s to Maven Central?", releaseNames), InputOption.YES); + if (InputOption.YES.equals(answer)) { + doPromote(repository, releaseNames); + } else { + LOGGER.info("Aborted."); + } + break; + case AUTO: + doPromote(repository, releaseNames); + break; + } + } catch (IOException e) { + LOGGER.warn("Failed executing command", e); + return CommandLine.ExitCode.SOFTWARE; + } + return CommandLine.ExitCode.OK; + } + + private void doPromote(StagingRepository repository, String releaseNames) throws IOException { + LOGGER.info("Promoting {} to Maven Central...", releaseNames); + repositoryService.promote(repository); + LOGGER.info("Done. Artifacts will appear on Maven Central within ~10 minutes."); + } +} diff --git a/src/test/java/org/apache/sling/cli/impl/release/DropCommandTest.java b/src/test/java/org/apache/sling/cli/impl/release/DropCommandTest.java new file mode 100644 index 0000000..171fa59 --- /dev/null +++ b/src/test/java/org/apache/sling/cli/impl/release/DropCommandTest.java @@ -0,0 +1,120 @@ +/* + * 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 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.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 DropCommandTest { + + @Rule + public final OsgiContext osgiContext = new OsgiContext(); + + @Rule + public final LogCapture logCapture = new LogCapture(DropCommand.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); + + 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()); + verify(repositoryService, never()).drop(any()); + assertTrue(logCapture.containsMessage("Would drop staging repository")); + } + + @Test + public void testInteractiveYes() throws Exception { + try (MockedStatic<UserInput> userInputMock = mockStatic(UserInput.class)) { + userInputMock.when(() -> UserInput.yesNo(anyString(), any())).thenReturn(InputOption.YES); + Command command = createCommand(123, ExecutionMode.INTERACTIVE); + assertEquals(CommandLine.ExitCode.OK, (int) command.call()); + verify(repositoryService, times(1)).drop(stagingRepository); + } + } + + @Test + public void testInteractiveNo() throws Exception { + try (MockedStatic<UserInput> userInputMock = mockStatic(UserInput.class)) { + userInputMock.when(() -> UserInput.yesNo(anyString(), any())).thenReturn(InputOption.NO); + Command command = createCommand(123, ExecutionMode.INTERACTIVE); + assertEquals(CommandLine.ExitCode.OK, (int) command.call()); + verify(repositoryService, never()).drop(any()); + assertTrue(logCapture.containsMessage("Aborted.")); + } + } + + @Test + public void testAuto() throws Exception { + Command command = createCommand(123, ExecutionMode.AUTO); + assertEquals(CommandLine.ExitCode.OK, (int) command.call()); + verify(repositoryService, times(1)).drop(stagingRepository); + } + + private Command createCommand(int repositoryId, ExecutionMode executionMode) throws IllegalAccessException { + DropCommand dropCommand = spy(new DropCommand()); + FieldUtils.writeField(dropCommand, "repositoryId", repositoryId, true); + ReusableCLIOptions reusableCLIOptions = mock(ReusableCLIOptions.class); + FieldUtils.writeField(reusableCLIOptions, "executionMode", executionMode, true); + FieldUtils.writeField(dropCommand, "reusableCLIOptions", reusableCLIOptions, true); + osgiContext.registerInjectActivateService(dropCommand); + Command result = osgiContext.getService(Command.class); + assertTrue( + "Expected to retrieve the DropCommand from the mocked OSGi environment.", + result instanceof DropCommand); + return result; + } +} diff --git a/src/test/java/org/apache/sling/cli/impl/release/PromoteCommandTest.java b/src/test/java/org/apache/sling/cli/impl/release/PromoteCommandTest.java new file mode 100644 index 0000000..166130e --- /dev/null +++ b/src/test/java/org/apache/sling/cli/impl/release/PromoteCommandTest.java @@ -0,0 +1,113 @@ +/* + * 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.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 PromoteCommandTest { + + @Rule + public final OsgiContext osgiContext = new OsgiContext(); + + @Rule + public final LogCapture logCapture = new LogCapture(PromoteCommand.class); + + private RepositoryService repositoryService; + private StagingRepository stagingRepository; + + @Before + public void before() throws Exception { + stagingRepository = mock(StagingRepository.class); + when(stagingRepository.getRepositoryId()).thenReturn("orgapachesling-123"); + + 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); + + 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()); + verify(repositoryService, never()).promote(any()); + assertTrue(logCapture.containsMessage("Would promote")); + } + + @Test + public void testInteractiveYes() throws Exception { + try (MockedStatic<UserInput> userInputMock = mockStatic(UserInput.class)) { + userInputMock.when(() -> UserInput.yesNo(anyString(), any())).thenReturn(InputOption.YES); + Command command = createCommand(123, ExecutionMode.INTERACTIVE); + assertEquals(CommandLine.ExitCode.OK, (int) command.call()); + verify(repositoryService, times(1)).promote(stagingRepository); + } + } + + @Test + public void testAuto() throws Exception { + Command command = createCommand(123, ExecutionMode.AUTO); + assertEquals(CommandLine.ExitCode.OK, (int) command.call()); + verify(repositoryService, times(1)).promote(stagingRepository); + } + + private Command createCommand(int repositoryId, ExecutionMode executionMode) throws IllegalAccessException { + PromoteCommand promoteCommand = spy(new PromoteCommand()); + FieldUtils.writeField(promoteCommand, "repositoryId", repositoryId, true); + ReusableCLIOptions reusableCLIOptions = mock(ReusableCLIOptions.class); + FieldUtils.writeField(reusableCLIOptions, "executionMode", executionMode, true); + FieldUtils.writeField(promoteCommand, "reusableCLIOptions", reusableCLIOptions, true); + osgiContext.registerInjectActivateService(promoteCommand); + Command result = osgiContext.getService(Command.class); + assertTrue( + "Expected to retrieve the PromoteCommand from the mocked OSGi environment.", + result instanceof PromoteCommand); + return result; + } +}
