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

Reply via email to