This is an automated email from the ASF dual-hosted git repository. rombert pushed a commit to branch feature/SLING-8337 in repository https://gitbox.apache.org/repos/asf/sling-org-apache-sling-committer-cli.git
commit abbd20f172a25e5e51ab75fa06c15fc1f8f9231f Author: Robert Munteanu <[email protected]> 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. --- .gitignore | 1 + Dockerfile | 30 ++++ README.md | 23 +++ bnd.bnd | 0 docker-env.sample | 15 ++ pom.xml | 173 +++++++++++++++++++++ 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 +++++ src/main/resources/conf/logback-default.xml | 23 +++ src/main/resources/scripts/launcher.sh | 29 ++++ .../impl/release/PrepareVoteEmailCommandTest.java | 31 ++++ 20 files changed, 1064 insertions(+) diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..a49f72d --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +docker-env diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..1338fcb --- /dev/null +++ b/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 [email protected] +# 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/README.md b/README.md new file mode 100644 index 0000000..eaa6d43 --- /dev/null +++ b/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/bnd.bnd b/bnd.bnd new file mode 100644 index 0000000..e69de29 diff --git a/docker-env.sample b/docker-env.sample new file mode 100644 index 0000000..15454cf --- /dev/null +++ b/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/pom.xml b/pom.xml new file mode 100644 index 0000000..047121d --- /dev/null +++ b/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/src/main/features/app.json b/src/main/features/app.json new file mode 100644 index 0000000..f07827d --- /dev/null +++ b/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/src/main/java/org/apache/sling/cli/impl/Command.java b/src/main/java/org/apache/sling/cli/impl/Command.java new file mode 100644 index 0000000..4caeea7 --- /dev/null +++ b/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/src/main/java/org/apache/sling/cli/impl/CommandProcessor.java b/src/main/java/org/apache/sling/cli/impl/CommandProcessor.java new file mode 100644 index 0000000..57e5430 --- /dev/null +++ b/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/src/main/java/org/apache/sling/cli/impl/ExecutionTrigger.java b/src/main/java/org/apache/sling/cli/impl/ExecutionTrigger.java new file mode 100644 index 0000000..23fa1b8 --- /dev/null +++ b/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/src/main/java/org/apache/sling/cli/impl/jira/Version.java b/src/main/java/org/apache/sling/cli/impl/jira/Version.java new file mode 100644 index 0000000..7cce8d5 --- /dev/null +++ b/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/src/main/java/org/apache/sling/cli/impl/jira/VersionFinder.java b/src/main/java/org/apache/sling/cli/impl/jira/VersionFinder.java new file mode 100644 index 0000000..5bf0406 --- /dev/null +++ b/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/src/main/java/org/apache/sling/cli/impl/nexus/StagingRepositories.java b/src/main/java/org/apache/sling/cli/impl/nexus/StagingRepositories.java new file mode 100644 index 0000000..84e1a77 --- /dev/null +++ b/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/src/main/java/org/apache/sling/cli/impl/nexus/StagingRepository.java b/src/main/java/org/apache/sling/cli/impl/nexus/StagingRepository.java new file mode 100644 index 0000000..167cebb --- /dev/null +++ b/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/src/main/java/org/apache/sling/cli/impl/nexus/StagingRepositoryFinder.java b/src/main/java/org/apache/sling/cli/impl/nexus/StagingRepositoryFinder.java new file mode 100644 index 0000000..3ef7992 --- /dev/null +++ b/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/src/main/java/org/apache/sling/cli/impl/release/PrepareVoteEmailCommand.java b/src/main/java/org/apache/sling/cli/impl/release/PrepareVoteEmailCommand.java new file mode 100644 index 0000000..eff3a3f --- /dev/null +++ b/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\" <[email protected]>\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/src/main/java/org/apache/sling/cli/impl/release/TallyVotesCommand.java b/src/main/java/org/apache/sling/cli/impl/release/TallyVotesCommand.java new file mode 100644 index 0000000..690a4d2 --- /dev/null +++ b/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/src/main/resources/conf/logback-default.xml b/src/main/resources/conf/logback-default.xml new file mode 100644 index 0000000..8f2963f --- /dev/null +++ b/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/src/main/resources/scripts/launcher.sh b/src/main/resources/scripts/launcher.sh new file mode 100755 index 0000000..6f0bcfb --- /dev/null +++ b/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/src/test/java/org/apache/sling/cli/impl/release/PrepareVoteEmailCommandTest.java b/src/test/java/org/apache/sling/cli/impl/release/PrepareVoteEmailCommandTest.java new file mode 100644 index 0000000..8dd81aa --- /dev/null +++ b/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")); + } +}
