This is an automated email from the ASF dual-hosted git repository. royteeuwen pushed a commit to branch feature/sling-cli-release-automation in repository https://gitbox.apache.org/repos/asf/sling-org-apache-sling-committer-cli.git
commit 0e0a5d5f58da9b60979976bf883a7bb82a638fe3 Author: Roy Teeuwen <[email protected]> AuthorDate: Sat May 30 08:09:57 2026 +0200 release close-staging: command with reactor-aware description Add the `close-staging` command and the RepositoryService support it needs: staging bulk actions (close/promote/drop), findAny() (which also resolves open repositories), and content-based release derivation that works on an open repo not yet in the Lucene index. The staging description is derived from the staged POM(s) instead of Nexus' generic "Implicitly created (auto staging)" text. When the staged POMs form a single multi-module reactor (one staged pom-packaging aggregator that is the parent of the other staged modules and is itself the top of the staged hierarchy), the release is that aggregator alone; otherwise each staged module becomes its own release. Versions inherited from <parent> are resolved so reactor children are not skipped. getReleases() and getReleasesFromContent() share this reduction so the closed- and open-repo paths agree. --- .../sling/cli/impl/nexus/RepositoryService.java | 252 +++++++++++++++++++-- .../cli/impl/release/CloseStagingCommand.java | 117 ++++++++++ .../cli/impl/nexus/RepositoryServiceTest.java | 41 ++++ .../cli/impl/release/CloseStagingCommandTest.java | 116 ++++++++++ 4 files changed, 504 insertions(+), 22 deletions(-) diff --git a/src/main/java/org/apache/sling/cli/impl/nexus/RepositoryService.java b/src/main/java/org/apache/sling/cli/impl/nexus/RepositoryService.java index 4625944..750c5ce 100644 --- a/src/main/java/org/apache/sling/cli/impl/nexus/RepositoryService.java +++ b/src/main/java/org/apache/sling/cli/impl/nexus/RepositoryService.java @@ -31,6 +31,8 @@ import java.io.InputStream; import java.io.InputStreamReader; import java.nio.file.Files; import java.nio.file.Path; +import java.util.ArrayList; +import java.util.Collections; import java.util.HashMap; import java.util.HashSet; import java.util.List; @@ -50,6 +52,9 @@ import org.apache.commons.io.IOUtils; import org.apache.http.HttpHeaders; import org.apache.http.client.methods.CloseableHttpResponse; import org.apache.http.client.methods.HttpGet; +import org.apache.http.client.methods.HttpPost; +import org.apache.http.entity.ContentType; +import org.apache.http.entity.StringEntity; import org.apache.http.impl.client.CloseableHttpClient; import org.apache.sling.cli.impl.ComponentContextHelper; import org.apache.sling.cli.impl.http.HttpClientFactory; @@ -111,6 +116,65 @@ public class RepositoryService { }); } + public StagingRepository findAny(int stagingRepositoryId) throws IOException { + return this.withStagingRepositories(reader -> { + Gson gson = new Gson(); + return gson.fromJson(reader, StagingRepositories.class).getData().stream() + .filter(r -> r.getRepositoryId().startsWith(REPOSITORY_PREFIX)) + .filter(r -> r.getRepositoryId().endsWith("-" + stagingRepositoryId)) + .findFirst() + .orElseThrow( + () -> new IllegalArgumentException("No repository found with id " + stagingRepositoryId)); + }); + } + + public void close(StagingRepository repository) throws IOException { + executeBulkAction("close", repository.getRepositoryId(), Collections.emptyMap()); + } + + public void close(StagingRepository repository, String description) throws IOException { + executeBulkAction("close", repository.getRepositoryId(), Collections.singletonMap("description", description)); + } + + public void promote(StagingRepository repository) throws IOException { + // Nexus "Release": move the staged artifacts to the release repository (which syncs to Maven + // Central) and drop the staging repository afterwards. This matches the payload the Nexus UI + // sends. Note there is no targetRepositoryId — that field is for build-promotion profiles and + // is rejected with HTTP 400 by the bulk/promote endpoint. + executeBulkAction( + "promote", repository.getRepositoryId(), Collections.singletonMap("autoDropAfterRelease", true)); + } + + public void drop(StagingRepository repository) throws IOException { + executeBulkAction("delete", repository.getRepositoryId(), Collections.emptyMap()); + } + + private void executeBulkAction(String action, String repositoryId, Map<String, Object> extraData) + throws IOException { + try (CloseableHttpClient client = httpClientFactory.newClient()) { + HttpPost post = new HttpPost(nexusUrlPrefix + "/service/local/staging/bulk/" + action); + post.addHeader(HttpHeaders.ACCEPT, CONTENT_TYPE_JSON); + + Map<String, Object> data = new HashMap<>(); + data.put("stagedRepositoryIds", Collections.singletonList(repositoryId)); + data.put("description", ""); + data.putAll(extraData); + + JsonObject body = new JsonObject(); + body.add("data", new Gson().toJsonTree(data)); + + post.setEntity(new StringEntity(body.toString(), ContentType.APPLICATION_JSON)); + + try (CloseableHttpResponse response = client.execute(post)) { + int statusCode = response.getStatusLine().getStatusCode(); + if (statusCode != 201) { + throw new IOException( + "Unexpected status " + statusCode + " for staging bulk/" + action + " on " + repositoryId); + } + } + } + } + private <T> T withStagingRepositories(Function<InputStreamReader, T> function) throws IOException { try (CloseableHttpClient client = httpClientFactory.newClient()) { HttpGet get = newGet("/service/local/staging/profile_repositories"); @@ -231,42 +295,186 @@ public class RepositoryService { } public Set<Release> getReleases(StagingRepository stagingRepository) throws IOException { - Set<Release> releases = new HashSet<>(); + List<PomCoordinates> poms = new ArrayList<>(); getArtifacts(stagingRepository).stream() .filter(artifact -> "pom".equals(artifact.getType())) .forEach(pom -> { try { - XPath xPath = xPathFactory.newXPath(); processArtifactStream(pom, stream -> { - try { - DocumentBuilder builder = builderFactory.newDocumentBuilder(); - Document xmlDocument = builder.parse(stream); - String name = (String) xPath.compile("/project/name/text()") - .evaluate(xmlDocument, XPathConstants.STRING); - String version = (String) xPath.compile("/project/version/text()") - .evaluate(xmlDocument, XPathConstants.STRING); - try { - releases.addAll(Release.fromString(name + " " + version)); - } catch (IllegalArgumentException e) { - LOGGER.error( - String.format( - "Unable to determine a valid release from '%s %s'", name, version), - e); - } - } catch (ParserConfigurationException - | SAXException - | XPathExpressionException - | IOException e) { - LOGGER.error(String.format("Unable to process artifact %s.", pom), e); + PomCoordinates coordinates = parsePom(stream, pom.toString()); + if (coordinates != null) { + poms.add(coordinates); } }); } catch (IOException e) { LOGGER.error(String.format("Unable to process artifact %s.", pom), e); } }); + return toReleases(poms); + } + + /** + * Determines the releases contained in a staging repository by browsing its content directly, + * rather than relying on the Nexus Lucene search index. This works for <em>open</em> staging + * repositories too, whereas {@link #getReleases(StagingRepository)} only sees repositories that + * have already been closed and indexed. + */ + public Set<Release> getReleasesFromContent(StagingRepository repository) throws IOException { + List<PomCoordinates> poms = new ArrayList<>(); + try (CloseableHttpClient client = httpClientFactory.newClient()) { + List<String> pomPaths = new ArrayList<>(); + collectPomPaths(client, repository.getRepositoryId(), "/org/apache/sling/", pomPaths); + for (String pomPath : pomPaths) { + HttpGet get = + newGet("/service/local/repositories/" + repository.getRepositoryId() + "/content" + pomPath); + try (CloseableHttpResponse response = client.execute(get)) { + if (response.getStatusLine().getStatusCode() != 200) { + continue; + } + try (InputStream stream = response.getEntity().getContent()) { + PomCoordinates coordinates = parsePom(stream, pomPath); + if (coordinates != null) { + poms.add(coordinates); + } + } + } + } + } + return toReleases(poms); + } + + private void collectPomPaths(CloseableHttpClient client, String repositoryId, String path, List<String> pomPaths) + throws IOException { + HttpGet get = newGet("/service/local/repositories/" + repositoryId + "/content" + path); + try (CloseableHttpResponse response = client.execute(get)) { + if (response.getStatusLine().getStatusCode() != 200) { + return; + } + try (InputStream content = response.getEntity().getContent(); + InputStreamReader reader = new InputStreamReader(content)) { + JsonArray data = new JsonParser() + .parse(reader) + .getAsJsonObject() + .get("data") + .getAsJsonArray(); + for (JsonElement element : data) { + JsonObject entry = element.getAsJsonObject(); + String relativePath = entry.get("relativePath").getAsString(); + if (entry.get("leaf").getAsBoolean()) { + if (entry.get("text").getAsString().endsWith(".pom")) { + pomPaths.add(relativePath); + } + } else { + collectPomPaths(client, repositoryId, relativePath, pomPaths); + } + } + } + } + } + + /** + * The Maven coordinates and name of a single staged POM, with versions resolved against the + * {@code <parent>} when a module inherits them. + */ + record PomCoordinates( + String name, String groupId, String artifactId, String version, String packaging, String parentKey) { + + /** {@code groupId:artifactId:version} identifying this artifact, or {@code null} if incomplete. */ + String ownKey() { + return coordinateKey(groupId, artifactId, version); + } + } + + private static String coordinateKey(String groupId, String artifactId, String version) { + if (groupId == null || groupId.isBlank() || artifactId == null || artifactId.isBlank()) { + return null; + } + return groupId + ":" + artifactId + ":" + version; + } + + private PomCoordinates parsePom(InputStream stream, String pomLabel) { + try { + XPath xPath = xPathFactory.newXPath(); + DocumentBuilder builder = builderFactory.newDocumentBuilder(); + Document doc = builder.parse(stream); + String name = xpathString(xPath, doc, "/project/name/text()"); + String artifactId = xpathString(xPath, doc, "/project/artifactId/text()"); + String groupId = xpathString(xPath, doc, "/project/groupId/text()"); + String version = xpathString(xPath, doc, "/project/version/text()"); + String packaging = xpathString(xPath, doc, "/project/packaging/text()"); + String parentGroupId = xpathString(xPath, doc, "/project/parent/groupId/text()"); + String parentArtifactId = xpathString(xPath, doc, "/project/parent/artifactId/text()"); + String parentVersion = xpathString(xPath, doc, "/project/parent/version/text()"); + // In a multi-module reactor a child module's POM frequently omits <groupId>/<version> and + // inherits them from its <parent>; fall back so such modules are not skipped. + if (groupId == null || groupId.isBlank()) { + groupId = parentGroupId; + } + if (version == null || version.isBlank()) { + version = parentVersion; + } + if (packaging == null || packaging.isBlank()) { + packaging = "jar"; + } + return new PomCoordinates( + name, + groupId, + artifactId, + version, + packaging, + coordinateKey(parentGroupId, parentArtifactId, parentVersion)); + } catch (ParserConfigurationException | SAXException | XPathExpressionException | IOException e) { + LOGGER.error(String.format("Unable to process pom %s.", pomLabel), e); + return null; + } + } + + private static String xpathString(XPath xPath, Document doc, String expression) throws XPathExpressionException { + return (String) xPath.compile(expression).evaluate(doc, XPathConstants.STRING); + } + + /** + * Reduces the staged POMs to the set of releases they represent. + * + * <p>When the staged POMs form a single multi-module reactor — i.e. there is exactly one staged + * {@code pom}-packaging aggregator that is the {@code <parent>} of other staged modules and is + * itself the top of the staged hierarchy — the release is that aggregator alone (the reactor is + * one logical release, tracked by one JIRA version). Otherwise (a single module, or several + * independent modules staged and voted together) every staged module becomes its own release. + */ + static Set<Release> toReleases(List<PomCoordinates> poms) { + Set<String> stagedKeys = + poms.stream().map(PomCoordinates::ownKey).filter(k -> k != null).collect(Collectors.toSet()); + List<PomCoordinates> aggregators = poms.stream() + .filter(p -> "pom".equals(p.packaging())) + // is the parent of at least one other staged module + .filter(p -> p.ownKey() != null + && poms.stream() + .anyMatch(other -> other != p && p.ownKey().equals(other.parentKey()))) + // and is the root of the staged hierarchy (its own parent is not itself staged, e.g. it + // is the shared org.apache.sling parent POM, which is not part of the release) + .filter(p -> p.parentKey() == null || !stagedKeys.contains(p.parentKey())) + .collect(Collectors.toList()); + if (aggregators.size() == 1) { + return buildReleases(aggregators.get(0)); + } + Set<Release> releases = new HashSet<>(); + for (PomCoordinates pom : poms) { + releases.addAll(buildReleases(pom)); + } return Set.copyOf(releases); } + private static Set<Release> buildReleases(PomCoordinates pom) { + try { + return new HashSet<>(Release.fromString(pom.name() + " " + pom.version())); + } catch (IllegalArgumentException e) { + LOGGER.error( + String.format("Unable to determine a valid release from '%s %s'", pom.name(), pom.version()), e); + return Set.of(); + } + } + private void downloadFileFromRepository( @NotNull StagingRepository repository, @NotNull CloseableHttpClient client, 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/nexus/RepositoryServiceTest.java b/src/test/java/org/apache/sling/cli/impl/nexus/RepositoryServiceTest.java index 58f1732..62bdb9b 100644 --- a/src/test/java/org/apache/sling/cli/impl/nexus/RepositoryServiceTest.java +++ b/src/test/java/org/apache/sling/cli/impl/nexus/RepositoryServiceTest.java @@ -170,6 +170,47 @@ public class RepositoryServiceTest { assertEquals("Sling Adapter Annotations 1.0.0", release.getFullName()); } + @Test + public void testToReleasesSingleModule() { + // a POM's <name> is the bare component name; the version comes from <version> + Set<Release> releases = RepositoryService.toReleases(List.of( + pom("Apache Sling Foo", "org.apache.sling", "org.apache.sling.foo", "1.2.0", "jar", PARENT_POM))); + assertEquals(Set.of("Apache Sling Foo 1.2.0"), fullNames(releases)); + } + + @Test + public void testToReleasesIndependentModulesAreAllReturned() { + // several unrelated modules staged + voted together: each keeps its own release/JIRA version + Set<Release> releases = RepositoryService.toReleases(List.of( + pom("Apache Sling Foo", "org.apache.sling", "org.apache.sling.foo", "1.2.0", "jar", PARENT_POM), + pom("Apache Sling Bar", "org.apache.sling", "org.apache.sling.bar", "2.0.0", "jar", PARENT_POM))); + assertEquals(Set.of("Apache Sling Foo 1.2.0", "Apache Sling Bar 2.0.0"), fullNames(releases)); + } + + @Test + public void testToReleasesReactorCollapsesToAggregator() { + // a real reactor: a pom-packaging aggregator that is the parent of the staged child modules. + // The whole reactor is one logical release -> only the aggregator's name is returned. + String reactorKey = "org.apache.sling:org.apache.sling.reactor:1.0.0"; + Set<Release> releases = RepositoryService.toReleases(List.of( + pom("Apache Sling Reactor", "org.apache.sling", "org.apache.sling.reactor", "1.0.0", "pom", PARENT_POM), + // children inherit groupId/version from the reactor parent + pom("Apache Sling Reactor Core", "org.apache.sling", "core", "1.0.0", "jar", reactorKey), + pom("Apache Sling Reactor API", "org.apache.sling", "api", "1.0.0", "jar", reactorKey))); + assertEquals(Set.of("Apache Sling Reactor 1.0.0"), fullNames(releases)); + } + + private static final String PARENT_POM = "org.apache.sling:sling:66"; + + private static RepositoryService.PomCoordinates pom( + String name, String groupId, String artifactId, String version, String packaging, String parentKey) { + return new RepositoryService.PomCoordinates(name, groupId, artifactId, version, packaging, parentKey); + } + + private static Set<String> fullNames(Set<Release> releases) { + return releases.stream().map(Release::getFullName).collect(java.util.stream.Collectors.toSet()); + } + private StagingRepository getStagingRepository() { StagingRepository stagingRepository = new StagingRepository(); stagingRepository.setRepositoryId("orgapachesling-0"); 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; + } +}
