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 2019/03/08 11:59:04 UTC
[sling-whiteboard] 01/02: SLING-8311 - Investigate creating a Sling
CLI tool for development task automation
This is an automated email from the ASF dual-hosted git repository.
rombert pushed a commit to branch feature/SLING-8311
in repository https://gitbox.apache.org/repos/asf/sling-whiteboard.git
commit 1a0d2ee48e2d820c641ebeee3740ff9af14cb47b
Author: Robert Munteanu <ro...@apache.org>
AuthorDate: Fri Mar 8 13:56:17 2019 +0200
SLING-8311 - Investigate creating a Sling CLI tool for development task automation
Prototype of sling cli tool.
---
cli/.gitignore | 1 +
cli/Dockerfile | 30 ++++
cli/README.md | 23 +++
cli/bnd.bnd | 0
cli/docker-env.sample | 15 ++
cli/pom.xml | 173 +++++++++++++++++++++
cli/src/main/features/app.json | 67 ++++++++
.../java/org/apache/sling/cli/impl/Command.java | 26 ++++
.../apache/sling/cli/impl/CommandProcessor.java | 143 +++++++++++++++++
.../apache/sling/cli/impl/ExecutionTrigger.java | 37 +++++
.../org/apache/sling/cli/impl/jira/Version.java | 47 ++++++
.../apache/sling/cli/impl/jira/VersionFinder.java | 98 ++++++++++++
.../sling/cli/impl/nexus/StagingRepositories.java | 33 ++++
.../sling/cli/impl/nexus/StagingRepository.java | 65 ++++++++
.../cli/impl/nexus/StagingRepositoryFinder.java | 86 ++++++++++
.../cli/impl/release/PrepareVoteEmailCommand.java | 98 ++++++++++++
.../sling/cli/impl/release/TallyVotesCommand.java | 39 +++++
cli/src/main/resources/conf/logback-default.xml | 23 +++
cli/src/main/resources/scripts/launcher.sh | 29 ++++
.../impl/release/PrepareVoteEmailCommandTest.java | 31 ++++
20 files changed, 1064 insertions(+)
diff --git a/cli/.gitignore b/cli/.gitignore
new file mode 100644
index 0000000..a49f72d
--- /dev/null
+++ b/cli/.gitignore
@@ -0,0 +1 @@
+docker-env
diff --git a/cli/Dockerfile b/cli/Dockerfile
new file mode 100644
index 0000000..1338fcb
--- /dev/null
+++ b/cli/Dockerfile
@@ -0,0 +1,30 @@
+# ----------------------------------------------------------------------------------------
+# 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.
+# ----------------------------------------------------------------------------------------
+
+FROM openjdk:8-jre-alpine
+MAINTAINER dev@sling.apache.org
+# escaping required to properly handle arguments with spaces
+ENTRYPOINT ["/usr/share/sling-cli/bin/launcher.sh"]
+
+# Add feature launcher
+ADD target/lib /usr/share/sling-cli/launcher
+# Add launcher script
+ADD target/classes/scripts /usr/share/sling-cli/bin
+# workaround for MRESOURCES-236
+RUN chmod a+x /usr/share/sling-cli/bin/*
+# Add config files
+ADD target/classes/conf /usr/share/sling-cli/conf
+# Add all bundles
+ADD target/artifacts /usr/share/sling-cli/artifacts
+# Add the service itself
+ARG FEATURE_FILE
+ADD ${FEATURE_FILE} /usr/share/sling-cli/sling-cli.feature
\ No newline at end of file
diff --git a/cli/README.md b/cli/README.md
new file mode 100644
index 0000000..eaa6d43
--- /dev/null
+++ b/cli/README.md
@@ -0,0 +1,23 @@
+# Apache Sling Engine CLI tool
+
+This module is part of the [Apache Sling](https://sling.apache.org) project.
+
+This module provides a command-line tool which automates various Sling development tasks. The tool is packaged
+as a docker image.
+
+## Configuration
+
+To make various credentials and configurations available to the docker image it is recommended to use a docker env file.
+A sample file is stored at `docker-env.sample`. Copy this file to `docker-env` and fill in your own information.
+
+## Launching
+
+The image is built using `mvn package`. Afterwards it may be run with
+
+ docker run --env-file=./docker-env apache/sling-cli
+
+This invocation produces a list of available subcommands.
+
+Currently the only implemented command is generating the release vote email, for instance
+
+ docker run --env-file=./docker-env apache/sling-cli release prepare-email $STAGING_REPOSITORY_ID
\ No newline at end of file
diff --git a/cli/bnd.bnd b/cli/bnd.bnd
new file mode 100644
index 0000000..e69de29
diff --git a/cli/docker-env.sample b/cli/docker-env.sample
new file mode 100644
index 0000000..15454cf
--- /dev/null
+++ b/cli/docker-env.sample
@@ -0,0 +1,15 @@
+#!/bin/sh
+# ----------------------------------------------------------------------------------------
+# 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.
+# ----------------------------------------------------------------------------------------
+ASF_USERNAME=changeme
+ASF_PASSWORD=changeme
+RELEASE_ID=42
diff --git a/cli/pom.xml b/cli/pom.xml
new file mode 100644
index 0000000..047121d
--- /dev/null
+++ b/cli/pom.xml
@@ -0,0 +1,173 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 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. -->
+<project xmlns="http://maven.apache.org/POM/4.0.0"
+ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+ xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+ <modelVersion>4.0.0</modelVersion>
+
+ <parent>
+ <groupId>org.apache.sling</groupId>
+ <artifactId>sling</artifactId>
+ <version>34</version>
+ <relativePath />
+ </parent>
+
+ <artifactId>sling-cli</artifactId>
+ <version>1.0-SNAPSHOT</version>
+
+ <description>Sling CLI tool for development usage</description>
+
+ <properties>
+ <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
+ </properties>
+
+ <build>
+ <plugins>
+ <plugin>
+ <groupId>biz.aQute.bnd</groupId>
+ <artifactId>bnd-maven-plugin</artifactId>
+ </plugin>
+ <plugin>
+ <artifactId>maven-jar-plugin</artifactId>
+ <configuration>
+ <archive>
+ <manifest>
+ <addClasspath>true</addClasspath>
+ <classpathPrefix>lib/</classpathPrefix>
+ <mainClass>org.apache.sling.cli.impl.Main</mainClass>
+ </manifest>
+ </archive>
+ </configuration>
+ </plugin>
+ <plugin>
+ <artifactId>maven-dependency-plugin</artifactId>
+ <executions>
+ <execution>
+ <phase>prepare-package</phase>
+ <goals>
+ <goal>copy-dependencies</goal>
+ </goals>
+ <configuration>
+ <overWriteReleases>false</overWriteReleases>
+ <includeScope>runtime</includeScope>
+ <outputDirectory>${project.build.directory}/lib</outputDirectory>
+ <stripVersion>true</stripVersion>
+ </configuration>
+ </execution>
+ </executions>
+ </plugin>
+ <plugin>
+ <groupId>org.apache.sling</groupId>
+ <artifactId>slingfeature-maven-plugin</artifactId>
+ <version>0.8.0</version>
+ <extensions>true</extensions>
+ <executions>
+ <execution>
+ <id>feature-dependencies</id>
+ <goals>
+ <goal>repository</goal>
+ </goals>
+ </execution>
+ <execution>
+ <id>extra-dependencies</id>
+ <goals>
+ <goal>repository</goal>
+ </goals>
+ <configuration>
+ <repositories>
+ <repository>
+ <embedArtifacts>
+ <embedArtifact>
+ <groupId>org.apache.felix</groupId>
+ <artifactId>org.apache.felix.framework</artifactId>
+ <version>6.0.2</version>
+ </embedArtifact>
+ <embedArtifact>
+ <groupId>org.apache.sling</groupId>
+ <artifactId>org.apache.sling.launchpad.api</artifactId>
+ <version>1.2.0</version>
+ </embedArtifact>
+ </embedArtifacts>
+ </repository>
+ </repositories>
+ </configuration>
+ </execution>
+ </executions>
+ </plugin>
+ <plugin>
+ <groupId>com.spotify</groupId>
+ <artifactId>dockerfile-maven-plugin</artifactId>
+ <version>1.4.10</version>
+ <executions>
+ <execution>
+ <id>default</id>
+ <goals>
+ <goal>build</goal>
+ </goals>
+ </execution>
+ </executions>
+ <configuration>
+ <skipDockerInfo>true</skipDockerInfo> <!-- does not contain legal files -->
+ <repository>apache/sling-cli</repository>
+ <buildArgs>
+ <FEATURE_FILE>target/artifacts/org/apache/sling/${project.artifactId}/${project.version}/${project.artifactId}-${project.version}-app.slingfeature</FEATURE_FILE>
+ </buildArgs>
+ </configuration>
+ </plugin>
+ </plugins>
+ </build>
+
+ <dependencies>
+ <dependency>
+ <groupId>org.osgi</groupId>
+ <artifactId>org.osgi.service.component.annotations</artifactId>
+ <scope>provided</scope>
+ </dependency>
+ <dependency>
+ <groupId>org.osgi</groupId>
+ <artifactId>org.osgi.service.metatype.annotations</artifactId>
+ <scope>provided</scope>
+ </dependency>
+ <dependency>
+ <groupId>org.osgi</groupId>
+ <artifactId>osgi.core</artifactId>
+ </dependency>
+ <dependency>
+ <groupId>org.apache.sling</groupId>
+ <artifactId>org.apache.sling.feature.launcher</artifactId>
+ <version>0.8.0</version>
+ <scope>runtime</scope>
+ </dependency>
+ <dependency>
+ <groupId>org.slf4j</groupId>
+ <artifactId>slf4j-api</artifactId>
+ <scope>provided</scope>
+ </dependency>
+ <dependency>
+ <groupId>org.apache.httpcomponents</groupId>
+ <artifactId>httpclient-osgi</artifactId>
+ <version>4.5.7</version>
+ <scope>provided</scope>
+ </dependency>
+ <dependency>
+ <groupId>com.google.code.gson</groupId>
+ <artifactId>gson</artifactId>
+ <version>2.8.5</version>
+ <scope>provided</scope>
+ </dependency>
+
+ <dependency>
+ <groupId>junit</groupId>
+ <artifactId>junit</artifactId>
+ </dependency>
+ </dependencies>
+</project>
diff --git a/cli/src/main/features/app.json b/cli/src/main/features/app.json
new file mode 100644
index 0000000..f07827d
--- /dev/null
+++ b/cli/src/main/features/app.json
@@ -0,0 +1,67 @@
+{
+ "id": "${project.groupId}:${project.artifactId}:slingfeature:app:${project.version}",
+ "variables": {
+ "asf.username":"change-me",
+ "asf.password": "change-me"
+ },
+ "bundles": [
+ {
+ "id": "${project.groupId}:${project.artifactId}:${project.version}",
+ "start-level": "5"
+ },
+ {
+ "id": "org.apache.felix:org.apache.felix.scr:2.1.12",
+ "start-level": "1"
+ },
+ {
+ "id": "org.apache.felix:org.apache.felix.configadmin:1.9.10",
+ "start-level": "1"
+ },
+ {
+ "id": "org.apache.felix:org.apache.felix.log:1.2.0",
+ "start-level": "1"
+ },
+ {
+ "id": "ch.qos.logback:logback-classic:1.2.3",
+ "start-level": "1"
+ },
+ {
+ "id": "ch.qos.logback:logback-core:1.2.3",
+ "start-level": "1"
+ },
+ {
+ "id": "org.slf4j:jul-to-slf4j:1.7.25",
+ "start-level": "1"
+ },
+ {
+ "id": "org.slf4j:jcl-over-slf4j:1.7.25",
+ "start-level": "1"
+ },
+ {
+ "id": "org.slf4j:slf4j-api:1.7.25",
+ "start-level": "1"
+ },
+ {
+ "id": "org.apache.felix:org.apache.felix.logback:1.0.2",
+ "start-level": "1"
+ },
+ {
+ "id": "org.apache.httpcomponents:httpcore-osgi:4.4.11",
+ "start-level": "3"
+ },
+ {
+ "id": "org.apache.httpcomponents:httpclient-osgi:4.5.7",
+ "start-level": "3"
+ },
+ {
+ "id": "com.google.code.gson:gson:2.8.5",
+ "start-level": "3"
+ }
+ ],
+ "configurations": {
+ "org.apache.sling.cli.impl.nexus.StagingRepositoryFinder": {
+ "username": "${asf.username}",
+ "password": "${asf.password}"
+ }
+ }
+}
\ No newline at end of file
diff --git a/cli/src/main/java/org/apache/sling/cli/impl/Command.java b/cli/src/main/java/org/apache/sling/cli/impl/Command.java
new file mode 100644
index 0000000..4caeea7
--- /dev/null
+++ b/cli/src/main/java/org/apache/sling/cli/impl/Command.java
@@ -0,0 +1,26 @@
+/*
+ * 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;
+
+public interface Command {
+
+ String PROPERTY_NAME_COMMAND = "command";
+ String PROPERTY_NAME_SUBCOMMAND = "subcommand";
+ String PROPERTY_NAME_SUMMARY = "summary";
+
+ void execute(String target);
+}
diff --git a/cli/src/main/java/org/apache/sling/cli/impl/CommandProcessor.java b/cli/src/main/java/org/apache/sling/cli/impl/CommandProcessor.java
new file mode 100644
index 0000000..57e5430
--- /dev/null
+++ b/cli/src/main/java/org/apache/sling/cli/impl/CommandProcessor.java
@@ -0,0 +1,143 @@
+/*
+ * 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;
+
+import static org.osgi.service.component.annotations.ReferenceCardinality.MULTIPLE;
+import static org.osgi.service.component.annotations.ReferencePolicy.DYNAMIC;
+
+import java.util.Map;
+import java.util.Objects;
+import java.util.concurrent.ConcurrentHashMap;
+
+import org.osgi.framework.BundleContext;
+import org.osgi.framework.BundleException;
+import org.osgi.framework.Constants;
+import org.osgi.framework.launch.Framework;
+import org.osgi.service.component.annotations.Component;
+import org.osgi.service.component.annotations.Reference;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+@Component(service = CommandProcessor.class)
+public class CommandProcessor {
+
+ private final Logger logger = LoggerFactory.getLogger(getClass());
+ private BundleContext ctx;
+
+ private Map<CommandKey, CommandWithProps> commands = new ConcurrentHashMap<>();
+
+ protected void activate(BundleContext ctx) {
+ this.ctx = ctx;
+ }
+
+ @Reference(service = Command.class, cardinality = MULTIPLE, policy = DYNAMIC)
+ protected void bindCommand(Command cmd, Map<String, ?> props) {
+ commands.put(CommandKey.of(props), CommandWithProps.of(cmd, props));
+ }
+
+ protected void unbindCommand(Map<String, ?> props) {
+ commands.remove(CommandKey.of(props));
+ }
+
+ public void runCommand() {
+ // TODO - remove duplication from CLI parsing code
+ CommandKey key = CommandKey.of(ctx.getProperty("exec.args"));
+ String target = parseTarget(ctx.getProperty("exec.args"));
+ commands.getOrDefault(key, new CommandWithProps(ignored -> {
+ logger.info("Usage: sling command sub-command [target]");
+ logger.info("");
+ logger.info("Available commands:");
+ commands.forEach((k, c) -> logger.info("{} {}: {}", k.command, k.subCommand, c.summary));
+ }, "")).cmd.execute(target);
+ try {
+ ctx.getBundle(Constants.SYSTEM_BUNDLE_LOCATION).adapt(Framework.class).stop();
+ } catch (BundleException e) {
+ logger.warn("Failed running command", e);
+ }
+ }
+
+ private String parseTarget(String cliSpec) {
+ if (cliSpec == null || cliSpec.isEmpty())
+ return null;
+
+ String[] args = cliSpec.split(" ");
+ if (args.length < 3)
+ return null;
+
+ return args[2];
+ }
+
+
+ static class CommandKey {
+
+ private static final CommandKey EMPTY = new CommandKey("", "");
+
+ private final String command;
+ private final String subCommand;
+
+ static CommandKey of(String cliSpec) {
+ if (cliSpec == null || cliSpec.isEmpty())
+ return EMPTY;
+
+ String[] args = cliSpec.split(" ");
+ if (args.length < 2)
+ return EMPTY;
+
+ return new CommandKey(args[0], args[1]);
+ }
+
+ static CommandKey of(Map<String, ?> serviceProps) {
+ return new CommandKey((String) serviceProps.get(Command.PROPERTY_NAME_COMMAND), (String) serviceProps.get(Command.PROPERTY_NAME_SUBCOMMAND));
+ }
+
+ CommandKey(String command, String subCommand) {
+ this.command = command;
+ this.subCommand = subCommand;
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(command, subCommand);
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (this == obj)
+ return true;
+ if (obj == null)
+ return false;
+ if (getClass() != obj.getClass())
+ return false;
+ CommandKey other = (CommandKey) obj;
+ return Objects.equals(command, other.command) && Objects.equals(subCommand, other.subCommand);
+ }
+ }
+
+ static class CommandWithProps {
+ private final Command cmd;
+ private final String summary;
+
+ static CommandWithProps of(Command cmd, Map<String, ?> props) {
+ return new CommandWithProps(cmd, (String) props.get(Command.PROPERTY_NAME_SUMMARY));
+ }
+
+ CommandWithProps(Command cmd, String summary) {
+ this.cmd = cmd;
+ this.summary = summary;
+ }
+ }
+}
diff --git a/cli/src/main/java/org/apache/sling/cli/impl/ExecutionTrigger.java b/cli/src/main/java/org/apache/sling/cli/impl/ExecutionTrigger.java
new file mode 100644
index 0000000..23fa1b8
--- /dev/null
+++ b/cli/src/main/java/org/apache/sling/cli/impl/ExecutionTrigger.java
@@ -0,0 +1,37 @@
+/*
+ * 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;
+
+import org.osgi.framework.BundleContext;
+import org.osgi.framework.FrameworkEvent;
+import org.osgi.service.component.annotations.Component;
+import org.osgi.service.component.annotations.Reference;
+
+@Component
+public class ExecutionTrigger {
+
+ @Reference
+ private CommandProcessor processor;
+
+ protected void activate(BundleContext ctx) {
+ ctx.addFrameworkListener(evt -> {
+ if (evt.getType() == FrameworkEvent.STARTED)
+ new Thread(() -> processor.runCommand(), getClass().getSimpleName() + "Thread").start();
+ });
+ // never removed but not important - it's one-shot anyway
+ }
+}
diff --git a/cli/src/main/java/org/apache/sling/cli/impl/jira/Version.java b/cli/src/main/java/org/apache/sling/cli/impl/jira/Version.java
new file mode 100644
index 0000000..7cce8d5
--- /dev/null
+++ b/cli/src/main/java/org/apache/sling/cli/impl/jira/Version.java
@@ -0,0 +1,47 @@
+/*
+ * 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.jira;
+
+public class Version {
+ private int id;
+ private String name;
+ private int issuesFixedCount;
+
+ public int getId() {
+ return id;
+ }
+
+ public void setId(int id) {
+ this.id = id;
+ }
+
+ public String getName() {
+ return name;
+ }
+
+ public void setName(String name) {
+ this.name = name;
+ }
+
+ public int getIssuesFixedCount() {
+ return issuesFixedCount;
+ }
+
+ public void setRelatedIssuesCount(int relatedIssuesCount) {
+ this.issuesFixedCount = relatedIssuesCount;
+ }
+}
diff --git a/cli/src/main/java/org/apache/sling/cli/impl/jira/VersionFinder.java b/cli/src/main/java/org/apache/sling/cli/impl/jira/VersionFinder.java
new file mode 100644
index 0000000..5bf0406
--- /dev/null
+++ b/cli/src/main/java/org/apache/sling/cli/impl/jira/VersionFinder.java
@@ -0,0 +1,98 @@
+/*
+ * 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.jira;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.lang.reflect.Type;
+import java.util.List;
+
+import org.apache.http.client.methods.CloseableHttpResponse;
+import org.apache.http.client.methods.HttpGet;
+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;
+import com.google.gson.reflect.TypeToken;
+
+@Component(service = VersionFinder.class)
+public class VersionFinder {
+
+ public Version find(String versionName) throws IOException {
+ Version version;
+
+ try (CloseableHttpClient client = HttpClients.createDefault()) {
+ version = findVersion(versionName, client);
+ populateRelatedIssuesCount(client, version);
+ }
+
+ return version;
+ }
+
+ private Version findVersion(String versionName, CloseableHttpClient client) throws IOException {
+ Version version;
+ HttpGet get = new HttpGet("https://issues.apache.org/jira/rest/api/2/project/SLING/versions");
+ get.addHeader("Accept", "application/json");
+ 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();
+ Type collectionType = TypeToken.getParameterized(List.class, Version.class).getType();
+ List<Version> versions = gson.fromJson(reader, collectionType);
+ version = versions.stream()
+ .filter(v -> v.getName().equals(versionName))
+ .findFirst()
+ .orElseThrow( () -> new IllegalArgumentException("No version found with name " + versionName));
+ }
+ }
+ return version;
+ }
+
+ private void populateRelatedIssuesCount(CloseableHttpClient client, Version version) throws IOException {
+
+ HttpGet get = new HttpGet("https://issues.apache.org/jira/rest/api/2/version/" + version.getId() +"/relatedIssueCounts");
+ get.addHeader("Accept", "application/json");
+ 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();
+ VersionRelatedIssuesCount issuesCount = gson.fromJson(reader, VersionRelatedIssuesCount.class);
+
+ version.setRelatedIssuesCount(issuesCount.getIssuesFixedCount());
+ }
+ }
+ }
+
+ static class VersionRelatedIssuesCount {
+
+ private int issuesFixedCount;
+
+ public int getIssuesFixedCount() {
+ return issuesFixedCount;
+ }
+
+ public void setIssuesFixedCount(int issuesFixedCount) {
+ this.issuesFixedCount = issuesFixedCount;
+ }
+ }
+}
diff --git a/cli/src/main/java/org/apache/sling/cli/impl/nexus/StagingRepositories.java b/cli/src/main/java/org/apache/sling/cli/impl/nexus/StagingRepositories.java
new file mode 100644
index 0000000..84e1a77
--- /dev/null
+++ b/cli/src/main/java/org/apache/sling/cli/impl/nexus/StagingRepositories.java
@@ -0,0 +1,33 @@
+/*
+ * 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.nexus;
+
+import java.util.List;
+
+public class StagingRepositories {
+
+ private List<StagingRepository> data;
+
+ public List<StagingRepository> getData() {
+ return data;
+ }
+
+ public void setData(List<StagingRepository> data) {
+ this.data = data;
+ }
+
+}
diff --git a/cli/src/main/java/org/apache/sling/cli/impl/nexus/StagingRepository.java b/cli/src/main/java/org/apache/sling/cli/impl/nexus/StagingRepository.java
new file mode 100644
index 0000000..167cebb
--- /dev/null
+++ b/cli/src/main/java/org/apache/sling/cli/impl/nexus/StagingRepository.java
@@ -0,0 +1,65 @@
+/*
+ * 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.nexus;
+
+/**
+ * DTO for GSON usage
+ *
+ */
+public class StagingRepository {
+
+ enum Status {
+ open, closed;
+ }
+
+ private String description;
+ private String repositoryId;
+ private String repositoryURI;
+ private Status type;
+
+ public String getDescription() {
+ return description;
+ }
+
+ public void setDescription(String description) {
+ this.description = description;
+ }
+
+ public String getRepositoryId() {
+ return repositoryId;
+ }
+
+ public void setRepositoryId(String repositoryId) {
+ this.repositoryId = repositoryId;
+ }
+
+ public String getRepositoryURI() {
+ return repositoryURI;
+ }
+
+ public void setRepositoryURI(String repositoryURI) {
+ this.repositoryURI = repositoryURI;
+ }
+
+ public Status getType() {
+ return type;
+ }
+
+ public void setType(Status type) {
+ this.type = type;
+ }
+}
diff --git a/cli/src/main/java/org/apache/sling/cli/impl/nexus/StagingRepositoryFinder.java b/cli/src/main/java/org/apache/sling/cli/impl/nexus/StagingRepositoryFinder.java
new file mode 100644
index 0000000..3ef7992
--- /dev/null
+++ b/cli/src/main/java/org/apache/sling/cli/impl/nexus/StagingRepositoryFinder.java
@@ -0,0 +1,86 @@
+/*
+ * 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.nexus;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+
+import org.apache.http.auth.AuthScope;
+import org.apache.http.auth.UsernamePasswordCredentials;
+import org.apache.http.client.methods.CloseableHttpResponse;
+import org.apache.http.client.methods.HttpGet;
+import org.apache.http.impl.client.BasicCredentialsProvider;
+import org.apache.http.impl.client.CloseableHttpClient;
+import org.apache.http.impl.client.HttpClients;
+import org.apache.sling.cli.impl.nexus.StagingRepository.Status;
+import org.osgi.service.component.annotations.Activate;
+import org.osgi.service.component.annotations.Component;
+import org.osgi.service.component.annotations.ConfigurationPolicy;
+import org.osgi.service.metatype.annotations.AttributeDefinition;
+import org.osgi.service.metatype.annotations.Designate;
+import org.osgi.service.metatype.annotations.ObjectClassDefinition;
+
+import com.google.gson.Gson;
+
+@Component(
+ configurationPolicy = ConfigurationPolicy.REQUIRE,
+ service = StagingRepositoryFinder.class
+)
+@Designate(ocd = StagingRepositoryFinder.Config.class)
+public class StagingRepositoryFinder {
+
+ @ObjectClassDefinition
+ static @interface Config {
+ @AttributeDefinition(name="Username")
+ String username();
+
+ @AttributeDefinition(name="Password")
+ String password();
+ }
+
+ private BasicCredentialsProvider credentialsProvider;
+
+ @Activate
+ protected void activate(Config cfg) {
+ credentialsProvider = new BasicCredentialsProvider();
+ credentialsProvider.setCredentials(new AuthScope("repository.apache.org", 443),
+ new UsernamePasswordCredentials(cfg.username(), cfg.password()));
+ }
+
+ public StagingRepository find(int stagingRepositoryId) throws IOException {
+ try ( CloseableHttpClient client = HttpClients.custom()
+ .setDefaultCredentialsProvider(credentialsProvider)
+ .build() ) {
+ HttpGet get = new HttpGet("https://repository.apache.org/service/local/staging/profile_repositories");
+ get.addHeader("Accept", "application/json");
+ 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, StagingRepositories.class).getData().stream()
+ .filter( r -> r.getType() == Status.closed)
+ .filter( r -> r.getRepositoryId().endsWith("-" + stagingRepositoryId))
+ .findFirst()
+ .orElseThrow(() -> new IllegalArgumentException("No repository found with id " + stagingRepositoryId));
+ }
+ }
+ }
+ }
+}
diff --git a/cli/src/main/java/org/apache/sling/cli/impl/release/PrepareVoteEmailCommand.java b/cli/src/main/java/org/apache/sling/cli/impl/release/PrepareVoteEmailCommand.java
new file mode 100644
index 0000000..eff3a3f
--- /dev/null
+++ b/cli/src/main/java/org/apache/sling/cli/impl/release/PrepareVoteEmailCommand.java
@@ -0,0 +1,98 @@
+/*
+ * 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.release;
+
+import java.io.IOException;
+
+import org.apache.sling.cli.impl.Command;
+import org.apache.sling.cli.impl.jira.Version;
+import org.apache.sling.cli.impl.jira.VersionFinder;
+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;
+
+@Component(service = Command.class, property = {
+ Command.PROPERTY_NAME_COMMAND + "=release",
+ Command.PROPERTY_NAME_SUBCOMMAND + "=prepare-email",
+ Command.PROPERTY_NAME_SUMMARY + "=Prepares an email vote for the specified release." })
+public class PrepareVoteEmailCommand implements Command {
+
+ // TODO - replace with file template
+ private static final String EMAIL_TEMPLATE ="To: \"Sling Developers List\" <de...@sling.apache.org>\n" +
+ "Subject: [VOTE] Release Apache Sling ##RELEASE_NAME##\n" +
+ "\n" +
+ "Hi,\n" +
+ "\n" +
+ "We solved ##FIXED_ISSUES_COUNT## issues in this release:\n" +
+ "https://issues.apache.org/jira/browse/SLING/fixforversion/##VERSION_ID##\n" +
+ "\n" +
+ "Staging repository:\n" +
+ "https://repository.apache.org/content/repositories/orgapachesling-##RELEASE_ID##/\n" +
+ "\n" +
+ "You can use this UNIX script to download the release and verify the signatures:\n" +
+ "https://gitbox.apache.org/repos/asf?p=sling-tooling-release.git;a=blob;f=check_staged_release.sh;hb=HEAD\n" +
+ "\n" +
+ "Usage:\n" +
+ "sh check_staged_release.sh ##RELEASE_ID## /tmp/sling-staging\n" +
+ "\n" +
+ "Please vote to approve this release:\n" +
+ "\n" +
+ " [ ] +1 Approve the release\n" +
+ " [ ] 0 Don't care\n" +
+ " [ ] -1 Don't release, because ...\n" +
+ "\n" +
+ "This majority vote is open for at least 72 hours.\n";
+
+ private final Logger logger = LoggerFactory.getLogger(getClass());
+
+ @Reference
+ private StagingRepositoryFinder repoFinder;
+
+ @Reference
+ private VersionFinder versionFinder;
+
+ @Override
+ public void execute(String target) {
+ try {
+ int repoId = Integer.parseInt(target);
+ StagingRepository repo = repoFinder.find(repoId);
+ String cleanVersion = getCleanVersion(repo.getDescription());
+ Version version = versionFinder.find(cleanVersion);
+
+ String emailContents = EMAIL_TEMPLATE
+ .replace("##RELEASE_NAME##", cleanVersion)
+ .replace("##RELEASE_ID##", String.valueOf(repoId))
+ .replace("##VERSION_ID##", String.valueOf(version.getId()))
+ .replace("##FIXED_ISSUES_COUNT##", String.valueOf(version.getIssuesFixedCount()));
+
+ logger.info(emailContents);
+
+ } catch (IOException e) {
+ logger.warn("Failed executing command", e);
+ }
+ }
+
+ static String getCleanVersion(String repoDescription) {
+ return repoDescription
+ .replace("Apache Sling ", "") // Apache Sling prefix
+ .replaceAll(" RC[0-9]*$", ""); // 'release candidate' suffix
+ }
+
+}
diff --git a/cli/src/main/java/org/apache/sling/cli/impl/release/TallyVotesCommand.java b/cli/src/main/java/org/apache/sling/cli/impl/release/TallyVotesCommand.java
new file mode 100644
index 0000000..690a4d2
--- /dev/null
+++ b/cli/src/main/java/org/apache/sling/cli/impl/release/TallyVotesCommand.java
@@ -0,0 +1,39 @@
+/*
+ * 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.release;
+
+import org.apache.sling.cli.impl.Command;
+import org.osgi.service.component.annotations.Component;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+@Component(service = Command.class, property = {
+ Command.PROPERTY_NAME_COMMAND+"=release",
+ Command.PROPERTY_NAME_SUBCOMMAND+"=tally-votes",
+ Command.PROPERTY_NAME_SUMMARY+"=Counts votes cast for a release and generates the result email"
+})
+public class TallyVotesCommand implements Command {
+
+ private final Logger logger = LoggerFactory.getLogger(getClass());
+
+ @Override
+ public void execute(String target) {
+ logger.info("Tallying votes for release {}", target);
+
+ }
+
+}
diff --git a/cli/src/main/resources/conf/logback-default.xml b/cli/src/main/resources/conf/logback-default.xml
new file mode 100644
index 0000000..8f2963f
--- /dev/null
+++ b/cli/src/main/resources/conf/logback-default.xml
@@ -0,0 +1,23 @@
+<!-- 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. -->
+<configuration>
+ <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
+ <encoder>
+ <pattern>%msg%n</pattern>
+ </encoder>
+ </appender>
+
+ <logger name="org.apache.sling.cli" level="INFO" />
+
+ <root level="WARN">
+ <appender-ref ref="STDOUT" />
+ </root>
+</configuration>
\ No newline at end of file
diff --git a/cli/src/main/resources/scripts/launcher.sh b/cli/src/main/resources/scripts/launcher.sh
new file mode 100755
index 0000000..6f0bcfb
--- /dev/null
+++ b/cli/src/main/resources/scripts/launcher.sh
@@ -0,0 +1,29 @@
+#!/bin/sh
+# ----------------------------------------------------------------------------------------
+# 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.
+# ----------------------------------------------------------------------------------------
+
+# TODO - contribute '-q' flag to launcher OR allow passthrough of org.slf4j.simpleLogger system properties
+
+
+# funky syntax needed to properly preserve arguments with whitespace
+ARGS_PROP="exec.args=$@"
+
+# Use exec to become pid 1, see https://docs.docker.com/develop/develop-images/dockerfile_best-practices/
+exec /usr/bin/java \
+ -Dorg.slf4j.simpleLogger.logFile=/dev/null \
+ -Dlogback.configurationFile=file:/usr/share/sling-cli/conf/logback-default.xml \
+ -jar /usr/share/sling-cli/launcher/org.apache.sling.feature.launcher.jar \
+ -f /usr/share/sling-cli/sling-cli.feature \
+ -c /usr/share/sling-cli/artifacts \
+ -D "$ARGS_PROP" \
+ -V "asf.username=${ASF_USERNAME}" \
+ -V "asf.password=${ASF_PASSWORD}"
\ No newline at end of file
diff --git a/cli/src/test/java/org/apache/sling/cli/impl/release/PrepareVoteEmailCommandTest.java b/cli/src/test/java/org/apache/sling/cli/impl/release/PrepareVoteEmailCommandTest.java
new file mode 100644
index 0000000..8dd81aa
--- /dev/null
+++ b/cli/src/test/java/org/apache/sling/cli/impl/release/PrepareVoteEmailCommandTest.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.release;
+
+import static org.junit.Assert.assertEquals;
+
+import org.junit.Test;
+
+public class PrepareVoteEmailCommandTest {
+
+ @Test
+ public void cleanVersion() {
+
+ assertEquals("Resource Merger 1.3.10",
+ PrepareVoteEmailCommand.getCleanVersion("Apache Sling Resource Merger 1.3.10 RC1"));
+ }
+}