This is an automated email from the ASF dual-hosted git repository. jsedding pushed a commit to branch support-multi-artifact-releases in repository https://gitbox.apache.org/repos/asf/sling-org-apache-sling-committer-cli.git
commit 1d6d3ee27b2e69b3e91d311abd3e774c51364c6e Author: Julian Sedding <[email protected]> AuthorDate: Mon Aug 24 14:45:41 2026 +0200 SLING-13319 - Support dist-promotion for multiple artifacts --- .../sling/cli/impl/release/FinalizeCommand.java | 38 +----- .../sling/cli/impl/release/UpdateDistCommand.java | 142 ++++++++++++++------- .../cli/impl/release/FinalizeCommandTest.java | 13 +- .../cli/impl/release/UpdateDistCommandTest.java | 78 +++++++++-- 4 files changed, 172 insertions(+), 99 deletions(-) diff --git a/src/main/java/org/apache/sling/cli/impl/release/FinalizeCommand.java b/src/main/java/org/apache/sling/cli/impl/release/FinalizeCommand.java index f6bba25..292e689 100644 --- a/src/main/java/org/apache/sling/cli/impl/release/FinalizeCommand.java +++ b/src/main/java/org/apache/sling/cli/impl/release/FinalizeCommand.java @@ -27,7 +27,6 @@ import org.apache.http.impl.client.CloseableHttpClient; import org.apache.sling.cli.impl.Command; import org.apache.sling.cli.impl.CredentialsService; import org.apache.sling.cli.impl.ExecutionMode; -import org.apache.sling.cli.impl.dist.DistRepository; import org.apache.sling.cli.impl.http.HttpClientFactory; import org.apache.sling.cli.impl.jira.Issue; import org.apache.sling.cli.impl.jira.VersionClient; @@ -288,7 +287,7 @@ public class FinalizeCommand implements Command { LOGGER.info("SKIPPED (staging repository already promoted; if dist still needs updating a PMC" + " member must run update-dist separately)"); } else { - stepUpdateDist(repository, mode); + stepUpdateDist(reusableCLIOptions.executionMode); } } @@ -305,39 +304,8 @@ public class FinalizeCommand implements Command { } } - private void stepUpdateDist(StagingRepository repository, ExecutionMode mode) throws IOException { - // Delegate the download/collect/publish flow to UpdateDistCommand so it is not duplicated here. - UpdateDistCommand.DistReleasePlan plan = UpdateDistCommand.planDistRelease(repositoryService, repository, null); - - if (plan.alreadyPublished()) { - LOGGER.info("dist/release already contains {} {}; skipping.", plan.artifactId(), plan.newVersion()); - return; - } - if (plan.newFiles().isEmpty()) { - LOGGER.warn( - "No artifacts were downloaded for {} {}; skipping dist update.", - plan.artifactId(), - plan.newVersion()); - return; - } - - if (mode == ExecutionMode.DRY_RUN) { - LOGGER.info( - "Would publish {} file(s) to dist/release for {} {}", - plan.newFiles().size(), - plan.artifactId(), - plan.newVersion()); - LOGGER.info( - "Would remove {} old file(s) from dist/release", - plan.oldFiles().size()); - } else { - DistRepository.publish( - plan.artifactId(), - plan.newVersion(), - plan.newFiles(), - plan.oldFiles(), - credentialsService.getAsfCredentials()); - } + private void stepUpdateDist(ExecutionMode mode) throws IOException { + UpdateDistCommand.doUpdateDist(repositoryService, repositoryId, null, mode, credentialsService); } private void stepCreateNextJiraVersion(Release release, ExecutionMode mode) throws IOException { diff --git a/src/main/java/org/apache/sling/cli/impl/release/UpdateDistCommand.java b/src/main/java/org/apache/sling/cli/impl/release/UpdateDistCommand.java index 8d8677b..9aad6c6 100644 --- a/src/main/java/org/apache/sling/cli/impl/release/UpdateDistCommand.java +++ b/src/main/java/org/apache/sling/cli/impl/release/UpdateDistCommand.java @@ -19,6 +19,7 @@ package org.apache.sling.cli.impl.release; import java.io.IOException; +import java.io.UncheckedIOException; import java.nio.file.Files; import java.nio.file.Path; import java.util.List; @@ -26,6 +27,7 @@ import java.util.stream.Stream; import org.apache.sling.cli.impl.Command; import org.apache.sling.cli.impl.CredentialsService; +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.dist.DistRepository; @@ -33,6 +35,7 @@ import org.apache.sling.cli.impl.nexus.Artifact; import org.apache.sling.cli.impl.nexus.LocalRepository; import org.apache.sling.cli.impl.nexus.RepositoryService; import org.apache.sling.cli.impl.nexus.StagingRepository; +import org.jetbrains.annotations.Nullable; import org.osgi.service.component.annotations.Component; import org.osgi.service.component.annotations.Reference; import org.slf4j.Logger; @@ -90,38 +93,44 @@ public class UpdateDistCommand implements Command { @Override public Integer call() { + Integer ok = doUpdateDist( + repositoryService, repositoryId, previousVersion, reusableCLIOptions.executionMode, credentialsService); + if (ok != null) return ok; + return CommandLine.ExitCode.OK; + } + + public static @Nullable Integer doUpdateDist( + RepositoryService repositoryService, + Integer repositoryId, + String previousVersion, + ExecutionMode executionMode, + CredentialsService credentialsService) { try { - DistReleasePlan plan = + List<DistReleasePlan> plans = planDistRelease(repositoryService, repositoryService.find(repositoryId), previousVersion); - if (plan.alreadyPublished()) { - LOGGER.info( - "dist/release already contains {} {}; nothing to do.", plan.artifactId(), plan.newVersion()); + if (plans.stream().allMatch(DistReleasePlan::alreadyPublished)) { + for (DistReleasePlan plan : plans) { + LOGGER.info( + "dist/release already contains {} {}; nothing to do.", + plan.artifactId(), + plan.newVersion()); + } return CommandLine.ExitCode.OK; } - if (plan.newFiles().isEmpty()) { + + boolean noArtifacts = plans.stream() + .flatMap(plan -> plan.newFiles().stream()) + .findFirst() + .isEmpty(); + if (noArtifacts) { LOGGER.warn("No artifacts were downloaded for staging repository {}.", repositoryId); return CommandLine.ExitCode.USAGE; } - switch (reusableCLIOptions.executionMode) { - case DRY_RUN: - LOGGER.info( - "Would publish {} file(s) to dist/release for {} {}:", - plan.newFiles().size(), - plan.artifactId(), - plan.newVersion()); - plan.newFiles() - .forEach(f -> LOGGER.info( - " put {} -> {}{}", f, DistRepository.DIST_RELEASE_URL, f.getFileName())); - if (!plan.oldFiles().isEmpty()) { - LOGGER.info( - "Would remove {} old file(s) from dist/release:", - plan.oldFiles().size()); - plan.oldFiles().forEach(f -> LOGGER.info(" rm {}", DistRepository.DIST_RELEASE_URL + f)); - } - break; - case INTERACTIVE: + for (DistReleasePlan plan : plans) { + boolean doPerformPublish = executionMode == ExecutionMode.AUTO; + if (executionMode == ExecutionMode.INTERACTIVE) { String question = String.format( "Publish %d file(s) for %s %s to dist/release and remove %d older file(s) for %s?", plan.newFiles().size(), @@ -129,31 +138,42 @@ public class UpdateDistCommand implements Command { plan.newVersion(), plan.oldFiles().size(), plan.artifactId()); - if (InputOption.YES.equals(UserInput.yesNo(question, InputOption.YES))) { - DistRepository.publish( - plan.artifactId(), - plan.newVersion(), - plan.newFiles(), - plan.oldFiles(), - credentialsService.getAsfCredentials()); - } else { + doPerformPublish = InputOption.YES.equals(UserInput.yesNo(question, InputOption.YES)); + if (!doPerformPublish) { LOGGER.info("Aborted."); } - break; - case AUTO: + } + + if (doPerformPublish) { DistRepository.publish( plan.artifactId(), plan.newVersion(), plan.newFiles(), plan.oldFiles(), credentialsService.getAsfCredentials()); - break; + } else { + LOGGER.info( + "Would publish {} file(s) to dist/release for {} {}:", + plan.newFiles().size(), + plan.artifactId(), + plan.newVersion()); + + plan.newFiles() + .forEach(f -> LOGGER.info( + " put {} -> {}{}", f, DistRepository.DIST_RELEASE_URL, f.getFileName())); + if (!plan.oldFiles().isEmpty()) { + LOGGER.info( + "Would remove {} old file(s) from dist/release:", + plan.oldFiles().size()); + plan.oldFiles().forEach(f -> LOGGER.info(" rm {}", DistRepository.DIST_RELEASE_URL + f)); + } + } } - } catch (IOException e) { + } catch (UncheckedIOException | IOException e) { LOGGER.warn("Failed executing command", e); return CommandLine.ExitCode.SOFTWARE; } - return CommandLine.ExitCode.OK; + return null; } /** What to publish to and remove from dist/release for one staged release. */ @@ -169,22 +189,50 @@ public class UpdateDistCommand implements Command { * Shared by this command and {@link FinalizeCommand} so the flow is not duplicated. When the version is * already present in {@code dist/release} the returned plan is marked {@link DistReleasePlan#alreadyPublished()}. */ - static DistReleasePlan planDistRelease( + static List<DistReleasePlan> planDistRelease( RepositoryService repositoryService, StagingRepository repository, String previousVersion) throws IOException { LocalRepository localRepository = repositoryService.download(repository); - Artifact primary = localRepository.getArtifacts().stream() + List<Artifact> artifacts = localRepository.getArtifacts().stream() .filter(a -> "pom".equals(a.getType())) - .findFirst() - .orElseThrow(() -> new IllegalStateException("No POM artifact found in staging repository")); - String artifactId = primary.getArtifactId(); - String newVersion = primary.getVersion(); - if (DistRepository.isVersionPublished(artifactId, newVersion)) { - return new DistReleasePlan(artifactId, newVersion, List.of(), List.of(), true); + .toList(); + + if (artifacts.isEmpty()) { + throw new IllegalStateException("No POM artifact found in staging repository"); + } + + List<DistReleasePlan> plans = artifacts.stream() + .filter(a -> { + try { + return DistRepository.isVersionPublished(a.getArtifactId(), a.getVersion()); + } catch (IOException e) { + throw new UncheckedIOException(e); + } + }) + .map(a -> new DistReleasePlan(a.getArtifactId(), a.getVersion(), List.of(), List.of(), true)) + .toList(); + + if (!plans.isEmpty()) { + return plans; + } else { + return artifacts.stream() + .map(a -> { + try { + String artifactId = a.getArtifactId(); + String newVersion = a.getVersion(); + List<Path> newFiles = collectDownloadedFiles(localRepository.getRootFolder()).stream() + .filter(path -> + path.getFileName().toString().startsWith(artifactId + "-" + newVersion)) + .toList(); + List<String> oldFiles = + DistRepository.listPreviousReleaseFiles(artifactId, newVersion, previousVersion); + return new DistReleasePlan(artifactId, newVersion, newFiles, oldFiles, false); + } catch (IOException e) { + throw new UncheckedIOException(e); + } + }) + .toList(); } - List<Path> newFiles = collectDownloadedFiles(localRepository.getRootFolder()); - List<String> oldFiles = DistRepository.listPreviousReleaseFiles(artifactId, newVersion, previousVersion); - return new DistReleasePlan(artifactId, newVersion, newFiles, oldFiles, false); } /** diff --git a/src/test/java/org/apache/sling/cli/impl/release/FinalizeCommandTest.java b/src/test/java/org/apache/sling/cli/impl/release/FinalizeCommandTest.java index 0b06b43..d044874 100644 --- a/src/test/java/org/apache/sling/cli/impl/release/FinalizeCommandTest.java +++ b/src/test/java/org/apache/sling/cli/impl/release/FinalizeCommandTest.java @@ -159,12 +159,12 @@ public class FinalizeCommandTest { try (MockedStatic<UpdateDistCommand> dist = mockStatic(UpdateDistCommand.class); MockedStatic<DistRepository> distRepo = mockStatic(DistRepository.class)) { dist.when(() -> UpdateDistCommand.planDistRelease(any(), any(), any())) - .thenReturn(new UpdateDistCommand.DistReleasePlan( + .thenReturn(List.of(new UpdateDistCommand.DistReleasePlan( "org.apache.sling.cli.test", "1.0.0", List.of(java.nio.file.Path.of("org.apache.sling.cli.test-1.0.0.pom")), List.of("org.apache.sling.cli.test-0.9.0.pom"), - false)); + false))); Command command = createCommand(123, ExecutionMode.DRY_RUN); assertEquals(CommandLine.ExitCode.OK, (int) command.call()); assertTrue(logCapture.containsMessage("--- Step 1/6: Update dist.apache.org ---")); @@ -193,14 +193,17 @@ public class FinalizeCommandTest { try (MockedStatic<UpdateDistCommand> dist = mockStatic(UpdateDistCommand.class); MockedStatic<DistRepository> distRepo = mockStatic(DistRepository.class)) { dist.when(() -> UpdateDistCommand.planDistRelease(any(), any(), any())) - .thenReturn(new UpdateDistCommand.DistReleasePlan( + .thenReturn(List.of(new UpdateDistCommand.DistReleasePlan( "org.apache.sling.cli.test", "1.0.0", List.of(java.nio.file.Path.of("org.apache.sling.cli.test-1.0.0.pom")), List.of("org.apache.sling.cli.test-0.9.0.pom"), - false)); + false))); + dist.when(() -> UpdateDistCommand.doUpdateDist(any(), any(), any(), any(), any())) + .thenCallRealMethod(); Command command = createCommand(123, ExecutionMode.AUTO); - assertEquals(CommandLine.ExitCode.OK, (int) command.call()); + int exitCode = command.call(); + assertEquals(CommandLine.ExitCode.OK, exitCode); verify(repositoryService).promote(any()); // dist upload is actually committed for a PMC member distRepo.verify( diff --git a/src/test/java/org/apache/sling/cli/impl/release/UpdateDistCommandTest.java b/src/test/java/org/apache/sling/cli/impl/release/UpdateDistCommandTest.java index 836a0df..c2ade92 100644 --- a/src/test/java/org/apache/sling/cli/impl/release/UpdateDistCommandTest.java +++ b/src/test/java/org/apache/sling/cli/impl/release/UpdateDistCommandTest.java @@ -21,6 +21,7 @@ package org.apache.sling.cli.impl.release; import java.io.ByteArrayInputStream; import java.io.File; import java.io.IOException; +import java.io.UncheckedIOException; import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.nio.file.Path; @@ -91,7 +92,7 @@ public class UpdateDistCommandTest { ARTIFACT + "-1.3.4.pom.asc", ARTIFACT + "-1.3.4-source-release.zip", ARTIFACT + "-1.3.4-source-release.zip.asc", - ARTIFACT + "-1.3.6.pom", // the new version - must be kept (not removed) + ARTIFACT + "-1.3.6.pom", // the new newVersion - must be kept (not removed) ARTIFACT + "-1.3.6.pom.asc", ARTIFACT + "-extra-1.0.0.pom" // a sibling artifact - must be ignored ); @@ -104,7 +105,7 @@ public class UpdateDistCommandTest { assertEquals(4, old.size()); assertTrue(old.contains(ARTIFACT + "-1.3.4.pom")); assertTrue(old.contains(ARTIFACT + "-1.3.4-source-release.zip")); - // the version being published is never removed + // the newVersion being published is never removed assertFalse(old.contains(ARTIFACT + "-1.3.6.pom")); assertFalse(old.contains(ARTIFACT + "-1.3.6.pom.asc")); // sibling artifact with a non-numeric component is ignored @@ -114,8 +115,8 @@ public class UpdateDistCommandTest { @Test public void testAutoDeduceDoesNotConfuseVersionPrefixesAndKeepsNewerVersions() throws Exception { - // publishing 1.0.14 must not treat 1.0.140 as the same version, and must not remove it either: - // 1.0.140 > 1.0.14, so it is a newer version and is left untouched (nothing older is present) + // publishing 1.0.14 must not treat 1.0.140 as the same newVersion, and must not remove it either: + // 1.0.140 > 1.0.14, so it is a newer newVersion and is left untouched (nothing older is present) List<String> releaseDir = List.of(ARTIFACT + "-1.0.140.pom", ARTIFACT + "-1.0.14.pom"); try (MockedStatic<DistRepository> dist = mockStatic(DistRepository.class, CALLS_REAL_METHODS)) { dist.when(() -> DistRepository.listFiles(eq(DistRepository.DIST_RELEASE_URL), anyString())) @@ -123,13 +124,13 @@ public class UpdateDistCommandTest { List<String> old = DistRepository.listPreviousReleaseFiles(ARTIFACT, "1.0.14", null); - assertTrue("a newer version must never be removed", old.isEmpty()); + assertTrue("a newer newVersion must never be removed", old.isEmpty()); } } @Test public void testAutoDeduceRemovesOnlyClosestOlderVersionAcrossStreams() throws Exception { - // parallel maintenance streams: publishing 2.0.4 must remove 2.0.2 (the closest older version) + // parallel maintenance streams: publishing 2.0.4 must remove 2.0.2 (the closest older newVersion) // but keep 1.2.4 (a different, still-maintained stream) List<String> releaseDir = List.of( ARTIFACT + "-1.2.4.pom", @@ -202,7 +203,49 @@ public class UpdateDistCommandTest { List<String> old = DistRepository.listPreviousReleaseFiles(ARTIFACT, "2.0.0", null); - assertTrue("a different major version must never be removed", old.isEmpty()); + assertTrue("a different major newVersion must never be removed", old.isEmpty()); + } + } + + @Test + public void testAutoDeduceIdentifiesMultiArtifactReleases() throws Exception { + List<ArtifactUpdate> artifacts = List.of( + new ArtifactUpdate("org.apache.sling.servlets.resolver", "2.12.0", "2.11.4"), + new ArtifactUpdate("org.apache.sling.servlets.resolver", "3.0.10", "3.0.8")); + + List<String> oldFiles = List.of( + "org.apache.sling.servlets.resolver-3.0.8.pom", + "org.apache.sling.servlets.resolver-3.0.8.pom.asc", + "org.apache.sling.servlets.resolver-3.0.8-source-release.zip", + "org.apache.sling.servlets.resolver-3.0.8-source-release.zip.asc", + "org.apache.sling.servlets.resolver-2.11.4.pom", + "org.apache.sling.servlets.resolver-2.11.4.pom.asc", + "org.apache.sling.servlets.resolver-2.11.4-source-release.zip", + "org.apache.sling.servlets.resolver-2.11.4-source-release.zip.asc", + + // unrelated files + "org.apache.sling.servlets.resolver-3.0.6.pom", + "org.apache.sling.servlets.resolver-3.0.6.pom.asc"); + + try (MockedStatic<DistRepository> dist = mockStatic(DistRepository.class, CALLS_REAL_METHODS)) { + dist.when(() -> DistRepository.listFiles(eq(DistRepository.DIST_RELEASE_URL), anyString())) + .thenReturn(oldFiles); + + artifacts.forEach(a -> { + try { + List<String> old = DistRepository.listPreviousReleaseFiles(a.artifactId(), a.newVersion(), null); + + assertEquals(4, old.size()); + + String oldArtifact = a.oldArtifact(); + assertTrue(old.contains(oldArtifact + ".pom")); + assertTrue(old.contains(oldArtifact + ".pom.asc")); + assertTrue(old.contains(oldArtifact + "-source-release.zip")); + assertTrue(old.contains(oldArtifact + "-source-release.zip.asc")); + } catch (IOException e) { + throw new UncheckedIOException(e); + } + }); } } @@ -215,7 +258,7 @@ public class UpdateDistCommandTest { List<String> old = DistRepository.listPreviousReleaseFiles(ARTIFACT, "1.3.6", "1.3.4"); assertEquals(List.of(ARTIFACT + "-1.3.4.pom"), old); - // when an explicit version is given, the directory is not enumerated with the bare prefix + // when an explicit newVersion is given, the directory is not enumerated with the bare prefix dist.verify( () -> DistRepository.listFiles(eq(DistRepository.DIST_RELEASE_URL), eq(ARTIFACT + "-")), never()); } @@ -223,10 +266,10 @@ public class UpdateDistCommandTest { @Test public void testAutoDeduceKeepsNewVersionWithClassifierAndExtension() throws Exception { - // files for the version being published (with both an extension '.' and a classifier '-' right - // after the version) must be kept, exercising belongsToVersion's trailing-character check + // files for the newVersion being published (with both an extension '.' and a classifier '-' right + // after the newVersion) must be kept, exercising belongsToVersion's trailing-character check List<String> releaseDir = List.of( - ARTIFACT + "-1.3.6", // exact match: filename equals the version prefix with no extension + ARTIFACT + "-1.3.6", // exact match: filename equals the newVersion prefix with no extension ARTIFACT + "-1.3.6.pom", ARTIFACT + "-1.3.6-source-release.zip", ARTIFACT + "-1.3.4.pom"); @@ -340,7 +383,7 @@ public class UpdateDistCommandTest { try (MockedStatic<DistRepository> dist = mockStatic(DistRepository.class, CALLS_REAL_METHODS)) { dist.when(() -> DistRepository.listFiles(eq(DistRepository.DIST_RELEASE_URL), eq(ARTIFACT + "-1.3.4"))) .thenReturn(List.of(ARTIFACT + "-1.3.4.pom")); - // the already-published probe queries the new version's prefix; it is not yet in dist/release + // the already-published probe queries the new newVersion's prefix; it is not yet in dist/release dist.when(() -> DistRepository.listFiles(eq(DistRepository.DIST_RELEASE_URL), eq(ARTIFACT + "-1.3.6"))) .thenReturn(List.of()); dist.when(() -> DistRepository.publish(any(), any(), any(), any(), any())) @@ -539,4 +582,15 @@ public class UpdateDistCommandTest { result instanceof UpdateDistCommand); return result; } + + private record ArtifactUpdate(String artifactId, String newVersion, String oldVersion) { + String oldArtifact() { + return artifactId() + "-" + oldVersion(); + } + + String newArtifact() { + return artifactId() + "-" + newVersion(); + } + } + ; }
