This is an automated email from the ASF dual-hosted git repository.

royteeuwen pushed a commit to branch bugfix/SLING-13320-nexus-error-response
in repository 
https://gitbox.apache.org/repos/asf/sling-org-apache-sling-committer-cli.git

commit 6b0bce74e03ff8244aef6b2c277e0cd2bc1c6569
Author: Roy Teeuwen <[email protected]>
AuthorDate: Tue Aug 25 21:07:54 2026 +0200

    SLING-13320 - handle a staging repository that no longer exists
    
    finalize promotes the staging repository in step 2 and Nexus drops it on
    release, so the website step in step 6 searched a repository that was
    already gone. Nexus answers that with an HTML error page, which parsed as
    JSON only produced "MalformedJsonException at line 2 column 4" and, being
    unchecked, took down the whole run after every irreversible step had
    already succeeded.
    
    * RepositoryService.getArtifacts checks the response status and reports
      the failure as an IOException naming the repository
    * UpdateLocalSiteCommand falls back to the released POMs on
      dist.apache.org when the staged ones cannot be read
    * FinalizeCommand treats the repository as gone once it has promoted it,
      and the website step no longer lets an unchecked exception fail
      finalize
    
    The mock Nexus now answers an unknown repository the way the real one
    does, so the regression test reproduces the reported exception exactly.
---
 .../sling/cli/impl/nexus/RepositoryService.java    | 10 ++++++
 .../sling/cli/impl/release/FinalizeCommand.java    | 20 ++++++++----
 .../cli/impl/release/UpdateLocalSiteCommand.java   | 19 ++++++++---
 .../cli/impl/nexus/QueryLuceneIndexHandler.java    | 36 ++++++++++++++++++++-
 .../cli/impl/nexus/RepositoryServiceTest.java      | 15 +++++++++
 .../cli/impl/release/FinalizeCommandTest.java      | 37 +++++++++++++++++++++-
 .../impl/release/UpdateLocalSiteCommandTest.java   | 25 +++++++++++++++
 7 files changed, 150 insertions(+), 12 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 9ee49bf..6e86d61 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
@@ -245,6 +245,16 @@ public class RepositoryService {
             HttpGet get = newGet(
                     
"/service/local/lucene/search?g=org.apache.sling&repositoryId=" + 
repository.getRepositoryId());
             try (CloseableHttpResponse response = client.execute(get)) {
+                // a repository Nexus does not know - typically one dropped 
right after promotion - is
+                // answered with an HTML error page, which parsed as JSON only 
yields an opaque
+                // MalformedJsonException (SLING-13320)
+                int statusCode = response.getStatusLine().getStatusCode();
+                if (statusCode != 200) {
+                    throw new IOException(String.format(
+                            "Got %d instead of 200 when searching for the 
artifacts of %s; Nexus answers 400 for"
+                                    + " a staging repository that no longer 
exists.",
+                            statusCode, repository.getRepositoryId()));
+                }
                 try (InputStream content = response.getEntity().getContent();
                         InputStreamReader reader = new 
InputStreamReader(content)) {
                     JsonParser parser = new JsonParser();
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..fc30ca0 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
@@ -214,8 +214,9 @@ public class FinalizeCommand implements Command {
         // resumable by --release. (PMC members only; auto-detected.)
         stepUpdateDistStage(repository, mode, isPmcMember);
 
-        // Step 2: Promote to Maven Central
-        stepPromoteStage(repository, mode);
+        // Step 2: Promote to Maven Central. Nexus drops the staging 
repository on release, so from here on
+        // it is gone and the later steps must treat it as such (SLING-13320).
+        repository = stepPromoteStage(repository, mode);
 
         // Step 3: Create next JIRA version and move unresolved issues 
(idempotent: skips if the successor
         // already exists / there are no unresolved issues left to move)
@@ -250,8 +251,9 @@ public class FinalizeCommand implements Command {
 
     /**
      * Delegates to {@link UpdateLocalSiteCommand} so the site editing, 
committing and pushing flow lives in
-     * one place. A failure here is reported but does not fail finalize: 
everything irreversible has already
-     * succeeded by this point, and the website can be updated separately with 
{@code update-local-site}.
+     * one place. Any failure here - unchecked ones included - is reported but 
does not fail finalize:
+     * everything irreversible has already succeeded by this point, and the 
website can be updated separately
+     * with {@code update-local-site}.
      */
     private void stepUpdateSite(StagingRepository repository, Set<Release> 
releases, ExecutionMode mode) {
         try {
@@ -267,7 +269,7 @@ public class FinalizeCommand implements Command {
                 LOGGER.warn(
                         "The downloads page has no entry for {} — please add 
it by hand.", update.downloadsNotListed());
             }
-        } catch (GitAPIException | IOException e) {
+        } catch (GitAPIException | IOException | RuntimeException e) {
             LOGGER.warn(
                     "Failed to update the Sling website; run '{} {}' 
separately to complete it.",
                     UpdateLocalSiteCommand.GROUP,
@@ -292,17 +294,23 @@ public class FinalizeCommand implements Command {
         }
     }
 
-    private void stepPromoteStage(StagingRepository repository, ExecutionMode 
mode) throws IOException {
+    /**
+     * Promotes the staging repository and returns the repository the later 
steps may still use: {@code null}
+     * once it has been promoted, since Nexus drops it on release and every 
request against it then fails.
+     */
+    private StagingRepository stepPromoteStage(StagingRepository repository, 
ExecutionMode mode) throws IOException {
         LOGGER.info("--- Step 2/6: Promote to Maven Central ---");
         if (repository == null) {
             LOGGER.info("SKIPPED (staging repository already promoted and 
dropped)");
         } else if (mode == ExecutionMode.DRY_RUN) {
             LOGGER.info("Would promote {} to Maven Central", 
repository.getRepositoryId());
+            return repository;
         } else {
             LOGGER.info("Promoting {}...", repository.getRepositoryId());
             repositoryService.promote(repository);
             LOGGER.info("Promoted. Artifacts will appear on Maven Central 
within ~10 minutes.");
         }
+        return null;
     }
 
     private void stepUpdateDist(StagingRepository repository, ExecutionMode 
mode) throws IOException {
diff --git 
a/src/main/java/org/apache/sling/cli/impl/release/UpdateLocalSiteCommand.java 
b/src/main/java/org/apache/sling/cli/impl/release/UpdateLocalSiteCommand.java
index 0b5ce65..02f9186 100644
--- 
a/src/main/java/org/apache/sling/cli/impl/release/UpdateLocalSiteCommand.java
+++ 
b/src/main/java/org/apache/sling/cli/impl/release/UpdateLocalSiteCommand.java
@@ -247,14 +247,25 @@ public class UpdateLocalSiteCommand extends 
AbstractReleaseCommand {
 
     /**
      * Resolves the artifact ids of {@code release}, preferring the staged 
POMs and falling back to the
-     * released POMs on dist.apache.org so a run after promotion still works.
+     * released POMs on dist.apache.org so a run after promotion still works — 
whether the staging
+     * repository is unset because the run resumes by name, or gone because 
this run just promoted it.
      */
     private static Set<String> resolveArtifactIds(
             RepositoryService repositoryService, StagingRepository repository, 
Release release) throws IOException {
         if (repository != null) {
-            Set<String> staged = repositoryService.getArtifactIds(repository, 
release);
-            if (!staged.isEmpty()) {
-                return new TreeSet<>(staged);
+            try {
+                Set<String> staged = 
repositoryService.getArtifactIds(repository, release);
+                if (!staged.isEmpty()) {
+                    return new TreeSet<>(staged);
+                }
+            } catch (IOException e) {
+                // Nexus drops the repository on promotion, so a run that 
promoted moments ago no longer
+                // finds its staged POMs; the released ones answer the same 
question (SLING-13320)
+                LOGGER.info(
+                        "Could not read the POMs staged in {} ({}); falling 
back to the released POMs on"
+                                + " dist.apache.org.",
+                        repository.getRepositoryId(),
+                        e.getMessage());
             }
         }
         List<String> candidates = 
DistRepository.listReleasePomFileNames(release.getVersion());
diff --git 
a/src/test/java/org/apache/sling/cli/impl/nexus/QueryLuceneIndexHandler.java 
b/src/test/java/org/apache/sling/cli/impl/nexus/QueryLuceneIndexHandler.java
index a17855f..70b842b 100644
--- a/src/test/java/org/apache/sling/cli/impl/nexus/QueryLuceneIndexHandler.java
+++ b/src/test/java/org/apache/sling/cli/impl/nexus/QueryLuceneIndexHandler.java
@@ -19,10 +19,13 @@
 package org.apache.sling.cli.impl.nexus;
 
 import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
 import java.nio.charset.StandardCharsets;
 import java.util.List;
 
 import com.sun.net.httpserver.HttpExchange;
+import org.apache.commons.io.IOUtils;
 import org.apache.commons.lang3.StringUtils;
 import org.apache.http.NameValuePair;
 import org.apache.http.client.utils.URLEncodedUtils;
@@ -55,7 +58,38 @@ public class QueryLuceneIndexHandler implements 
HttpExchangeHandler {
             LOGGER.warn("Expected a group (g) parameter. Skipping handler.");
             return false;
         }
-        serveFileFromClasspath(ex, "/nexus/" + repositoryId + "/lucene.json");
+        try (InputStream in = getClass().getResourceAsStream("/nexus/" + 
repositoryId + "/lucene.json")) {
+            if (in == null) {
+                serveRepositoryGone(ex);
+                return true;
+            }
+            ex.sendResponseHeaders(200, 0);
+            try (OutputStream out = ex.getResponseBody()) {
+                IOUtils.copy(in, out);
+            }
+        }
         return true;
     }
+
+    /**
+     * Answers exactly like Nexus does for a repository it does not know - a 
staging repository dropped after
+     * promotion, most notably: HTTP 400 with an HTML error page rather than 
JSON (SLING-13320).
+     */
+    private void serveRepositoryGone(HttpExchange ex) throws IOException {
+        byte[] body = ("<html>\n"
+                        + "  <head>\n"
+                        + "    <title>400 - Bad Request</title>\n"
+                        + "  </head>\n"
+                        + "  <body>\n"
+                        + "    <h1>400 - Bad Request</h1>\n"
+                        + "    <p>Repository to be searched does not 
exists!</p>\n"
+                        + "  </body>\n"
+                        + "</html>")
+                .getBytes(StandardCharsets.UTF_8);
+        ex.getResponseHeaders().add("Content-Type", "text/html");
+        ex.sendResponseHeaders(400, body.length);
+        try (OutputStream out = ex.getResponseBody()) {
+            out.write(body);
+        }
+    }
 }
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 2acff01..7b4dde9 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
@@ -88,6 +88,21 @@ public class RepositoryServiceTest {
         assertEquals(5, artifacts.size());
     }
 
+    @Test
+    public void testLuceneSearchOfADroppedRepositoryFailsWithAReadableError() {
+        // Nexus answers an unknown repository - one dropped after promotion, 
typically - with an HTML
+        // error page; parsing that as JSON produced an opaque 
MalformedJsonException (SLING-13320)
+        StagingRepository dropped = new StagingRepository();
+        dropped.setRepositoryId("orgapachesling-3121");
+        try {
+            repositoryService.getArtifacts(dropped);
+            fail("Expected an IOException for a repository Nexus no longer 
knows.");
+        } catch (IOException e) {
+            assertTrue(e.getMessage(), e.getMessage().contains("400"));
+            assertTrue(e.getMessage(), 
e.getMessage().contains("orgapachesling-3121"));
+        }
+    }
+
     @Test
     public void testRepositoryFind() throws IOException {
         StagingRepository stagingRepository = repositoryService.find(0);
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..17fb881 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
@@ -70,6 +70,7 @@ public class FinalizeCommandTest {
     @Rule
     public final LogCapture logCapture = new LogCapture(FinalizeCommand.class);
 
+    private StagingRepository stagingRepository;
     private RepositoryService repositoryService;
     private VersionClient versionClient;
     private CloseableHttpClient client;
@@ -97,7 +98,7 @@ public class FinalizeCommandTest {
      * a PMC member, which drives the dist.apache.org step.
      */
     private void prepare(boolean pmcMember) throws Exception {
-        StagingRepository stagingRepository = mock(StagingRepository.class);
+        stagingRepository = mock(StagingRepository.class);
         
when(stagingRepository.getRepositoryId()).thenReturn("orgapachesling-123");
         when(stagingRepository.getDescription()).thenReturn("Apache Sling CLI 
Test 1.0.0");
 
@@ -222,6 +223,40 @@ public class FinalizeCommandTest {
         site.verify(() -> UpdateLocalSiteCommand.applySiteUpdate(any(), any(), 
eq(ExecutionMode.AUTO), any(), any()));
     }
 
+    @Test
+    public void testSiteUpdateDoesNotUseTheRepositoryPromoteJustDropped() 
throws Exception {
+        prepare(false); // non-PMC: the dist step is skipped, so this test 
needs no network
+        Command command = createCommand(123, ExecutionMode.AUTO);
+        assertEquals(CommandLine.ExitCode.OK, (int) command.call());
+
+        // Nexus drops the staging repository on release, so step 6 must 
resolve the artifact ids from
+        // dist.apache.org instead of searching a repository that is gone 
(SLING-13320)
+        site.verify(() -> UpdateLocalSiteCommand.updateLocalSite(any(), 
isNull(), any(), any()));
+    }
+
+    @Test
+    public void testDryRunKeepsTheRepositoryForTheSiteStep() throws Exception {
+        prepare(false);
+        // nothing was promoted, so the staged POMs are still the best source 
for the artifact ids
+        Command command = createCommand(123, ExecutionMode.DRY_RUN);
+        assertEquals(CommandLine.ExitCode.OK, (int) command.call());
+
+        site.verify(() -> UpdateLocalSiteCommand.updateLocalSite(any(), 
eq(stagingRepository), any(), any()));
+    }
+
+    @Test
+    public void testSiteUpdateRuntimeFailureDoesNotFailFinalize() throws 
Exception {
+        prepare(false);
+        // an unchecked exception escaping step 6 used to fail the whole run 
after everything irreversible
+        // had already succeeded (SLING-13320)
+        site.when(() -> UpdateLocalSiteCommand.updateLocalSite(any(), any(), 
any(), any()))
+                .thenThrow(new com.google.gson.JsonSyntaxException("malformed 
JSON"));
+
+        Command command = createCommand(123, ExecutionMode.AUTO);
+        assertEquals(CommandLine.ExitCode.OK, (int) command.call());
+        assertTrue(logCapture.containsMessage("Failed to update the Sling 
website"));
+    }
+
     @Test
     public void testSiteUpdateFailureDoesNotFailFinalize() throws Exception {
         prepare(false); // non-PMC: the dist step is skipped, so this test 
needs no network
diff --git 
a/src/test/java/org/apache/sling/cli/impl/release/UpdateLocalSiteCommandTest.java
 
b/src/test/java/org/apache/sling/cli/impl/release/UpdateLocalSiteCommandTest.java
index f198e0c..998b643 100644
--- 
a/src/test/java/org/apache/sling/cli/impl/release/UpdateLocalSiteCommandTest.java
+++ 
b/src/test/java/org/apache/sling/cli/impl/release/UpdateLocalSiteCommandTest.java
@@ -139,6 +139,31 @@ public class UpdateLocalSiteCommandTest {
         
assertTrue(downloads().contains("\"Event|org.apache.sling.event|4.4.2|"));
     }
 
+    @Test
+    public void testAnUnreadableStagingRepositoryFallsBackToTheReleasedPoms() 
throws Exception {
+        // finalize promotes before updating the site, and Nexus drops the 
repository on release; the
+        // staged POMs are then unreachable, so the released ones must answer 
instead (SLING-13320)
+        RepositoryService repositoryService = mock(RepositoryService.class);
+        StagingRepository repository = mock(StagingRepository.class);
+        when(repository.getRepositoryId()).thenReturn("orgapachesling-3121");
+        when(repositoryService.find(123)).thenReturn(repository);
+        when(repositoryService.getReleases(repository))
+                .thenReturn(Set.copyOf(Release.fromString("Apache Sling Event 
Impl 4.4.2")));
+        when(repositoryService.getArtifactIds(eq(repository), any()))
+                .thenThrow(new IOException("Got 400 instead of 200 for 
orgapachesling-3121"));
+        when(repositoryService.getArtifactIdsFromPomUrls(any(), any(), any()))
+                .thenReturn(Set.of("org.apache.sling.event"));
+        registerServices(repositoryService);
+
+        try (MockedStatic<DistRepository> dist = stubDist("4.4.2")) {
+            Command command = createCommand(123, null, ExecutionMode.DRY_RUN);
+            assertEquals(CommandLine.ExitCode.OK, (int) command.call());
+        }
+
+        assertTrue(logCapture.containsMessage("falling back to the released 
POMs on dist.apache.org"));
+        
assertTrue(downloads().contains("\"Event|org.apache.sling.event|4.4.2|"));
+    }
+
     @Test
     public void 
testMaintenanceReleaseOfAnOlderMajorLeavesTheDownloadsPageAlone() throws 
Exception {
         // the fixture lists Resource Resolver at 1.6.6; releasing 2.0.0 must 
not rewrite that entry

Reply via email to