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:03 UTC

[sling-whiteboard] branch feature/SLING-8311 created (now f4d1203)

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

rombert pushed a change to branch feature/SLING-8311
in repository https://gitbox.apache.org/repos/asf/sling-whiteboard.git.


      at f4d1203  SLING-8311 - Investigate creating a Sling CLI tool for development task automation

This branch includes the following new commits:

     new 1a0d2ee  SLING-8311 - Investigate creating a Sling CLI tool for development task automation
     new f4d1203  SLING-8311 - Investigate creating a Sling CLI tool for development task automation

The 2 revisions listed above as "new" are entirely new to this
repository and will be described in separate emails.  The revisions
listed as "add" were already present in the repository and have only
been added to this reference.



[sling-whiteboard] 01/02: SLING-8311 - Investigate creating a Sling CLI tool for development task automation

Posted by ro...@apache.org.
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"));
+    }
+}


[sling-whiteboard] 02/02: SLING-8311 - Investigate creating a Sling CLI tool for development task automation

Posted by ro...@apache.org.
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 f4d1203dd2990186fd751d959f220e98dbe3eec5
Author: Robert Munteanu <ro...@apache.org>
AuthorDate: Fri Mar 8 13:58:39 2019 +0200

    SLING-8311 - Investigate creating a Sling CLI tool for development task automation
    
    Add cli to the reactor
---
 pom.xml | 1 +
 1 file changed, 1 insertion(+)

diff --git a/pom.xml b/pom.xml
index 309581b..0132385 100644
--- a/pom.xml
+++ b/pom.xml
@@ -40,6 +40,7 @@
 
     <modules>
           <module>mdresourceprovider</module>
+          <module>cli</module>
     </modules>
 </project>