SLING-8311 - Investigate creating a Sling CLI tool for development task automation
Implement command for generating release result email, still WIP.
diff --git a/README.md b/README.md
index af04f3f..eabf20c 100644
--- a/README.md
+++ b/README.md
@@ -18,10 +18,16 @@
This invocation produces a list of available subcommands.
-Currently the only implemented command is generating the release vote email, for instance
+## Commands
+
+Generating a release vote email
docker run --env-file=./docker-env apache/sling-cli release prepare-email $STAGING_REPOSITORY_ID
+Generating a release vote result email
+
+ docker run --env-file=./docker-env apache/sling-cli release tally-votes $STAGING_REPOSITORY_ID
+
## Assumptions
This tool assumes that the name of the staging repository matches the one of the version in Jira. For instance, the
diff --git a/docker-env.sample b/docker-env.sample
index 15454cf..7a2a892 100644
--- a/docker-env.sample
+++ b/docker-env.sample
@@ -12,4 +12,3 @@
# ----------------------------------------------------------------------------------------
ASF_USERNAME=changeme
ASF_PASSWORD=changeme
-RELEASE_ID=42
diff --git a/src/main/java/org/apache/sling/cli/impl/mail/Email.java b/src/main/java/org/apache/sling/cli/impl/mail/Email.java
new file mode 100644
index 0000000..54ec66e
--- /dev/null
+++ b/src/main/java/org/apache/sling/cli/impl/mail/Email.java
@@ -0,0 +1,48 @@
+/*
+ * 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.mail;
+
+public class Email {
+ private String from;
+ private String subject;
+ private String body;
+
+ public String getFrom() {
+ return from;
+ }
+
+ public void setFrom(String from) {
+ this.from = from;
+ }
+
+ public String getSubject() {
+ return subject;
+ }
+
+ public void setSubject(String subject) {
+ this.subject = subject;
+ }
+
+ public String getBody() {
+ return body;
+ }
+
+ public void setBody(String body) {
+ this.body = body;
+ }
+
+}
diff --git a/src/main/java/org/apache/sling/cli/impl/mail/EmailThread.java b/src/main/java/org/apache/sling/cli/impl/mail/EmailThread.java
new file mode 100644
index 0000000..03ac673
--- /dev/null
+++ b/src/main/java/org/apache/sling/cli/impl/mail/EmailThread.java
@@ -0,0 +1,31 @@
+/*
+ * 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.mail;
+
+import java.util.List;
+
+public class EmailThread {
+ private List<Email> emails;
+
+ public List<Email> getEmails() {
+ return emails;
+ }
+
+ public void setEmails(List<Email> emails) {
+ this.emails = emails;
+ }
+}
diff --git a/src/main/java/org/apache/sling/cli/impl/mail/VoteThreadFinder.java b/src/main/java/org/apache/sling/cli/impl/mail/VoteThreadFinder.java
new file mode 100644
index 0000000..0d39968
--- /dev/null
+++ b/src/main/java/org/apache/sling/cli/impl/mail/VoteThreadFinder.java
@@ -0,0 +1,61 @@
+/*
+ * 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.mail;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.net.URI;
+import java.net.URISyntaxException;
+
+import org.apache.http.client.methods.CloseableHttpResponse;
+import org.apache.http.client.methods.HttpGet;
+import org.apache.http.client.utils.URIBuilder;
+import org.apache.http.impl.client.CloseableHttpClient;
+import org.apache.http.impl.client.HttpClients;
+import org.osgi.service.component.annotations.Component;
+
+import com.google.gson.Gson;
+
+@Component(service = VoteThreadFinder.class)
+public class VoteThreadFinder {
+
+ public EmailThread findVoteThread(String releaseName) throws IOException {
+ try ( CloseableHttpClient client = HttpClients.createDefault() ) {
+
+ URI uri = new URIBuilder("https://lists.apache.org/api/stats.lua")
+ .addParameter("domain", "sling.apache.org")
+ .addParameter("list", "dev")
+ .addParameter("d", "lte=1M")
+ .addParameter("q", "[VOTE] Release " + releaseName)
+ .build();
+
+ HttpGet get = new HttpGet(uri);
+ try ( CloseableHttpResponse response = client.execute(get)) {
+ try ( InputStream content = response.getEntity().getContent();
+ InputStreamReader reader = new InputStreamReader(content)) {
+ if ( response.getStatusLine().getStatusCode() != 200 )
+ throw new IOException("Status line : " + response.getStatusLine());
+ Gson gson = new Gson();
+ return gson.fromJson(reader, EmailThread.class);
+ }
+ }
+ } catch (URISyntaxException e) {
+ throw new IllegalArgumentException(e);
+ }
+ }
+}
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 690a4d2..8e34d87 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
@@ -16,8 +16,18 @@
*/
package org.apache.sling.cli.impl.release;
+import java.io.IOException;
+import java.util.Arrays;
+import java.util.stream.Collectors;
+
import org.apache.sling.cli.impl.Command;
+import org.apache.sling.cli.impl.mail.Email;
+import org.apache.sling.cli.impl.mail.EmailThread;
+import org.apache.sling.cli.impl.mail.VoteThreadFinder;
+import org.apache.sling.cli.impl.nexus.StagingRepository;
+import org.apache.sling.cli.impl.nexus.StagingRepositoryFinder;
import org.osgi.service.component.annotations.Component;
+import org.osgi.service.component.annotations.Reference;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -27,13 +37,66 @@
Command.PROPERTY_NAME_SUMMARY+"=Counts votes cast for a release and generates the result email"
})
public class TallyVotesCommand implements Command {
-
+
+ // TODO - move to file
+ private static final String EMAIL_TEMPLATE ="\n" +
+ "\n" +
+ "To: \"Sling Developers List\" <dev@sling.apache.org>\n" +
+ "Subject: [RESULT] [VOTE] Release Apache Sling ##RELEASE_NAME##\n" +
+ "\n" +
+ "Hi,\n" +
+ "\n" +
+ "The vote has passed with the following result :\n" +
+ "\n" +
+ "+1 (binding): ##BINDING_VOTERS##\n" +
+ "\n" +
+ "I will copy this release to the Sling dist directory and\n" +
+ "promote the artifacts to the central Maven repository.\n";
private final Logger logger = LoggerFactory.getLogger(getClass());
+ @Reference
+ private StagingRepositoryFinder repoFinder;
+
+ @Reference
+ private VoteThreadFinder voteThreadFinder;
+
@Override
public void execute(String target) {
- logger.info("Tallying votes for release {}", target);
+ try {
+
+ StagingRepository repository = repoFinder.find(Integer.parseInt(target));
+ // TODO - release name cleanup does not belong here
+ String releaseName = repository.getDescription().replaceFirst(" RC[0-9]+", "");
+ EmailThread voteThread = voteThreadFinder.findVoteThread(releaseName);
+ // TODO - validate which voters are binding and list them separately in the email
+ String bindingVoters = voteThread.getEmails().stream()
+ .filter( e -> isPositiveVote(e) )
+ .map ( e -> e.getFrom().replaceAll("<.*>", "").trim() )
+ .collect(Collectors.joining(", "));
+
+ String email = EMAIL_TEMPLATE
+ .replace("##RELEASE_NAME##", releaseName)
+ .replace("##BINDING_VOTERS##", bindingVoters);
+
+ logger.info(email);
+
+ } catch (IOException e) {
+ logger.warn("Command execution failed", e);
+ }
+ }
+
+ // TODO - better detection of '+1' votes
+ private boolean isPositiveVote(Email e) {
+ return cleanup(e.getBody()).contains("+1");
+ }
+
+ private String cleanup(String subject) {
+ String[] lines = subject.split("\\n");
+ return Arrays.stream(lines)
+ .filter( l -> !l.isEmpty() )
+ .filter( l -> !l.startsWith(">"))
+ .collect(Collectors.joining("\n"));
}
}