You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@sling.apache.org by ro...@apache.org on 2026/08/30 20:47:46 UTC
(sling-org-apache-sling-committer-cli) 02/02: SLING-13321 - do not count the result email of an earlier run as a vote
This is an automated email from the ASF dual-hosted git repository.
royteeuwen pushed a commit to branch feature/SLING-13321-result-email-in-reply-to
in repository https://gitbox.apache.org/repos/asf/sling-org-apache-sling-committer-cli.git
commit bd1a28a01bb24ea0aa4b636e4498c6b90685be56
Author: Roy Teeuwen <ro...@apache.org>
AuthorDate: Sun Aug 30 22:44:36 2026 +0200
SLING-13321 - do not count the result email of an earlier run as a vote
The [RESULT] email repeats the tally it announces ("+1 (binding): ..."),
which the "+1" detection counted as a vote cast by the release manager
who sent it. Re-running tally-votes for a release therefore credited the
release manager with a vote they never cast, and put them in the binding
list when they are a PMC member.
Verified against the archive: re-running tally-votes for Resource
Resolver 1.12.16 added Roy Teeuwen as a non-binding voter and for Feature
Model Analyser 2.0.16 added Robert Munteanu as a fourth binding voter,
neither of whom had voted. Both tallies now match the result emails that
were actually sent.
---
.../sling/cli/impl/release/TallyVotesCommand.java | 23 ++++++++++++
.../cli/impl/release/TallyVotesCommandTest.java | 41 ++++++++++++++++++++--
2 files changed, 61 insertions(+), 3 deletions(-)
diff --git a/src/main/java/org/apache/sling/cli/impl/release/TallyVotesCommand.java b/src/main/java/org/apache/sling/cli/impl/release/TallyVotesCommand.java
index 2f88c9b..50caaae 100644
--- a/src/main/java/org/apache/sling/cli/impl/release/TallyVotesCommand.java
+++ b/src/main/java/org/apache/sling/cli/impl/release/TallyVotesCommand.java
@@ -28,6 +28,7 @@ import java.util.LinkedHashSet;
import java.util.List;
import java.util.Locale;
import java.util.Set;
+import java.util.regex.Pattern;
import java.util.stream.Collectors;
import org.apache.commons.io.IOUtils;
@@ -97,6 +98,13 @@ public class TallyVotesCommand implements Command {
/** Prefix of the subject of the email that opens a vote thread. */
private static final String VOTE_SUBJECT_PREFIX = "[VOTE]";
+ /** Prefix of the subject of the email that announces the outcome of a vote. */
+ private static final String RESULT_SUBJECT_PREFIX = "[RESULT]";
+
+ /** The reply and forward prefixes mail clients prepend to the subject of a reply. */
+ private static final Pattern REPLY_SUBJECT_PREFIX =
+ Pattern.compile("^\\s*(?:(?:re|fw|fwd|aw)\\s*:\\s*)+", Pattern.CASE_INSENSITIVE);
+
/** The steps {@link FinalizeCommand} performs, in the order it performs them. */
private static final String FINALIZE_STEPS = " 1. copy the artifacts to the Sling dist directory\n"
+ " (https://dist.apache.org/repos/dist/release/sling/)\n"
@@ -143,6 +151,7 @@ public class TallyVotesCommand implements Command {
Email threadStart = voteEmail != null ? voteEmail : emailThread.get(0);
emailThread.stream()
.filter(email -> email != threadStart)
+ .filter(email -> !isResultEmail(email))
.filter(this::isPositiveVote)
.forEachOrdered(email -> {
String from = email.getFrom().getAddress();
@@ -241,6 +250,20 @@ public class TallyVotesCommand implements Command {
.orElse(null);
}
+ /**
+ * Returns whether the email announces the outcome of the vote rather than casting one. The result
+ * email of an earlier run repeats the tally it announced ("+1 (binding): ..."), which the "+1"
+ * detection would otherwise count as a vote cast by the release manager who sent it.
+ *
+ * @param email the email to inspect
+ * @return {@code true} if the email is a result email or a reply to one
+ */
+ private boolean isResultEmail(Email email) {
+ String subject = email.getSubject();
+ return subject != null
+ && REPLY_SUBJECT_PREFIX.matcher(subject).replaceFirst("").startsWith(RESULT_SUBJECT_PREFIX);
+ }
+
/**
* Builds the headers that make the result email a reply to the vote email, so that both are part
* of a single thread. Returns an empty string when threading is disabled or when the vote email
diff --git a/src/test/java/org/apache/sling/cli/impl/release/TallyVotesCommandTest.java b/src/test/java/org/apache/sling/cli/impl/release/TallyVotesCommandTest.java
index 124faeb..65ecb10 100644
--- a/src/test/java/org/apache/sling/cli/impl/release/TallyVotesCommandTest.java
+++ b/src/test/java/org/apache/sling/cli/impl/release/TallyVotesCommandTest.java
@@ -252,7 +252,7 @@ public class TallyVotesCommandTest {
prepareExecution(mailer, thread);
Command command = createCommand(123, ExecutionMode.AUTO, true);
assertEquals(CommandLine.ExitCode.OK, (int) command.call());
- verify(mailer).send(expectedEmailWithoutReplyHeaders());
+ verify(mailer).send(expectedEmail(""));
}
@Test
@@ -270,7 +270,7 @@ public class TallyVotesCommandTest {
prepareExecution(mailer, thread);
Command command = createCommand(123, ExecutionMode.AUTO);
assertEquals(CommandLine.ExitCode.OK, (int) command.call());
- verify(mailer).send(expectedEmailWithoutReplyHeaders());
+ verify(mailer).send(expectedEmail(""));
assertTrue(logCapture.containsMessage(
"The [VOTE] email has no Message-ID; the result email will not be sent as a reply."));
}
@@ -295,12 +295,13 @@ public class TallyVotesCommandTest {
assertTrue(logCapture.containsMessage("Could not identify the [VOTE] email in the thread for release"));
}
- private String expectedEmailWithoutReplyHeaders() {
+ private String expectedEmail(String replyHeaders) {
return """
From: John Doe <jo...@apache.org>
To: "Sling Developers List" <de...@sling.apache.org>
Reply-To: "Sling Developers List" <de...@sling.apache.org>
Date: Thu, 1 Jan 1970 01:00:00 +0100
+ """ + replyHeaders + """
Subject: [RESULT] [VOTE] Release Apache Sling CLI Test 1.0.0
Hi,
@@ -325,6 +326,30 @@ public class TallyVotesCommandTest {
""";
}
+ @Test
+ public void testAutoIgnoresResultEmailOfAnEarlierRun() throws Exception {
+ // Re-running tally-votes finds the result email of the previous run in the thread. Its body
+ // repeats the tally it announced, so it must not be counted as a vote by the member who sent
+ // it: Daniel never voted here and must not show up as a voter.
+ List<Email> thread = new ArrayList<>() {
+ {
+ add(mockVoteEmail("johndoe@apache.org", "John Doe"));
+ add(mockEmail("alice@apache.org", "Alice"));
+ add(mockEmail("bob@apache.org", "Bob"));
+ add(mockEmail("charlie@apache.org", "Charlie"));
+ add(mockResultEmail("daniel@apache.org", "Daniel", "[RESULT] "));
+ add(mockResultEmail("daniel@apache.org", "Daniel", "Re: [RESULT] "));
+ }
+ };
+ Mailer mailer = mock(Mailer.class);
+ prepareExecution(mailer, thread);
+ Command command = createCommand(123, ExecutionMode.AUTO);
+ assertEquals(CommandLine.ExitCode.OK, (int) command.call());
+ verify(mailer)
+ .send(expectedEmail(
+ "In-Reply-To: " + VOTE_MESSAGE_ID + "\n" + "References: " + VOTE_MESSAGE_ID + "\n"));
+ }
+
private Command createCommand(int repositoryId, ExecutionMode executionMode) throws IllegalAccessException {
return createCommand(repositoryId, executionMode, false);
}
@@ -353,6 +378,16 @@ public class TallyVotesCommandTest {
return email;
}
+ private Email mockResultEmail(String address, String name, String subjectPrefix) throws Exception {
+ Email email = mock(Email.class);
+ when(email.getBody())
+ .thenReturn("The vote has passed with the following result:\n\n"
+ + "+1 (binding): Alice, Bob, Charlie\n+1 (non-binding): none\n");
+ when(email.getFrom()).thenReturn(new InternetAddress(address, name));
+ when(email.getSubject()).thenReturn(subjectPrefix + "[VOTE] Release Apache Sling CLI Test 1.0.0");
+ return email;
+ }
+
private Email mockEmail(String address, String name) throws Exception {
Email email = mock(Email.class);
when(email.getBody()).thenReturn("+1");