You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@sling.apache.org by js...@apache.org on 2026/08/24 12:45:57 UTC
(sling-org-apache-sling-committer-cli) 01/01: SLING-13319 - Support dist-promotion for multiple artifacts
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 <js...@apache.org>
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();
+ }
+ }
+ ;
}