From fb44e65e62440be5042a2dae2f9d6e96fef292b3 Mon Sep 17 00:00:00 2001 From: Marcin Grzejszczak Date: Thu, 7 Sep 2017 12:56:09 +0200 Subject: [PATCH] Introduction of non interactive mode (#45) without this change the only way to execute tasks is to provide their number from the console with this change we can provide execution options from the command line, we can execute full releases, single steps, multiple steps, bounded and unbounded ranges fixes #37 fixes #44 prerequisite for #30 --- README.adoc | 122 +++++++- .../asciidoc/spring-cloud-release-tools.adoc | 122 +++++++- spring-cloud-release-tools-spring/pom.xml | 6 + .../release/internal/ReleaserApplication.java | 7 +- .../release/internal/options/Options.java | 24 ++ .../internal/options/OptionsBuilder.java | 42 +++ .../release/internal/options/Parser.java | 10 + .../cloud/release/internal/spring/Args.java | 33 ++ .../internal/spring/OptionsParser.java | 110 +++++++ .../internal/spring/OptionsProcessor.java | 180 +++++++++++ .../spring/ReleaserConfiguration.java | 5 + .../internal/spring/SpringReleaser.java | 194 ++---------- .../cloud/release/internal/spring/Task.java | 65 ++++ .../cloud/release/internal/spring/Tasks.java | 99 ++++++ .../internal/spring/AcceptanceTests.java | 16 +- .../spring/OptionsProcessorTests.java | 290 ++++++++++++++++++ 16 files changed, 1145 insertions(+), 180 deletions(-) create mode 100644 spring-cloud-release-tools-spring/src/main/java/org/springframework/cloud/release/internal/options/Options.java create mode 100644 spring-cloud-release-tools-spring/src/main/java/org/springframework/cloud/release/internal/options/OptionsBuilder.java create mode 100644 spring-cloud-release-tools-spring/src/main/java/org/springframework/cloud/release/internal/options/Parser.java create mode 100644 spring-cloud-release-tools-spring/src/main/java/org/springframework/cloud/release/internal/spring/Args.java create mode 100644 spring-cloud-release-tools-spring/src/main/java/org/springframework/cloud/release/internal/spring/OptionsParser.java create mode 100644 spring-cloud-release-tools-spring/src/main/java/org/springframework/cloud/release/internal/spring/OptionsProcessor.java create mode 100644 spring-cloud-release-tools-spring/src/main/java/org/springframework/cloud/release/internal/spring/Task.java create mode 100644 spring-cloud-release-tools-spring/src/main/java/org/springframework/cloud/release/internal/spring/Tasks.java create mode 100644 spring-cloud-release-tools-spring/src/test/java/org/springframework/cloud/release/internal/spring/OptionsProcessorTests.java diff --git a/README.adoc b/README.adoc index f4f7f553..ac183749 100644 --- a/README.adoc +++ b/README.adoc @@ -41,7 +41,7 @@ why this tool makes it easy to automate the release / dependency update process - Generates a tweet template under `target/tweet.txt` (ONLY FOR NON-SNAPSHOT VERSIONS) - Generates a release notes template under `target/notes.md` (ONLY FOR NON-SNAPSHOT VERSIONS) -=== How to run it +=== What should I do first? Members of the Spring Cloud Team typically use this tool as follows. They first clone the releaser locally and build the jar manually @@ -53,6 +53,8 @@ $ cd spring-cloud-release-tools $ ./mvnw clean install ---- +=== How to run it (interactive mode) + Go to your project (e.g. Spring Cloud Sleuth) [source,bash] @@ -79,21 +81,133 @@ You will see text similar to this one 2) Update poms with versions from Spring Cloud Release 3) Build the project 4) Commit, tag and push the tag -5) Deploy the artifacts to Artifactory +5) Deploy the artifacts 6) Publish the docs 7) Go back to snapshots and bump originalVersion by patch 8) Push the commits 9) Close the milestone at Github -10) Create email / tweet etc. templates +10) Create email / blog / tweet etc. templates +You can pick a range of options by using the hyphen - e.g. '2-4' will execute jobs [2,3,4] +You can execute all tasks starting from a job by using a hyphen and providing only one number - e.g. '8-' will execute jobs [8,9,10] +You can execute given tasks by providing a colon separated list of tasks - e.g. '3,7,8' will execute jobs [3,7,8] You can press 'q' to quit ---- -Just pick a number and continue! Pick either a full release or single steps. +Just pick a number and continue! Pick either a full release or single steps. You can also pick +ranges or multiple steps. You can also provide the range only with the starting step +- that you will execute all steps starting from the given one. TIP: Read before picking a number cause it might have changed between tool releases ;) +=== How to run it (automatic mode) + +Go to your project (e.g. Spring Cloud Sleuth) and execute the application with `-h` or `--help` +flag. + +[source,bash] +---- +$ git clone git@github.com:spring-cloud/spring-cloud-sleuth.git +$ cd spring-cloud-sleuth +$ # example of running the releaser agains Dalston.SR1 tag with 1.0.0.BUILD-SNAPSHOT version of the releaser +$ java -jar ~/repo/spring-cloud-release-tools/spring-cloud-release-tools-spring/target/spring-cloud-release-tools-spring-1.0.0.BUILD-SNAPSHOT.jar --releaser.pom.branch=vDalston.SR1 --spring.config.name=releaser -h +---- + +You will see a help screen looking like more or less like this + +[source,bash] +---- +Here you can find the list of tasks in order + +[updatePoms,build,commit,deploy,docs,snapshots,push,closeMilestone,createTemplates] + + +Option Description +------ ----------- +-a, --start-from Starts all release task starting from the given + task. Requires passing the task name (either + one letter or the full name) +-b, --build [String] Build the project +-c, --commit [String] Commit, tag and push the tag +-d, --deploy [String] Deploy the artifacts +-f, --full-release [Boolean] Do you want to do the full release (default: + false) +-h, --help [String] +-i, --interactive Do you want to set the properties from the + command line (default: true) +-m, --closeMilestone [String] Close the milestone at Github +-o, --docs [String] Publish the docs +-p, --push [String] Push the commits +-r, --range Runs release tasks from the given range. + Requires passing the task names with a + hyphen. The first task is inclusive, the + second inclusive. E.g. 's-m' would mean + running 'snapshot', 'push' and 'milestone' + tasks +-s, --snapshots [String] Go back to snapshots and bump originalVersion + by patch +-t, --createTemplates [String] Create email / blog / tweet etc. templates +-u, --updatePoms [String] Update poms with versions from Spring Cloud + Release + +Examples of usage: + +Run 'build' & 'commit' & 'deploy' +java -jar releaser.jar -b -c -d + +Start from 'push' +java -jar releaser.jar -a push + +Range 'docs' -> 'push' +java -jar releaser.jar -r o-p +---- + +The Releaser can use two sets of options. The configuration options like `releaser.pom.branch` +and the task switches. For the tasks you can use either the full names or short switches. For example + providing range of tasks via switches `o-p` is equivalent to full name `docs-push`. + +A couple of examples: + +.Doing the full release in interactive mode (asking for skipping steps) +[source,bash] +---- +$ git clone git@github.com:spring-cloud/spring-cloud-sleuth.git +$ cd spring-cloud-sleuth +$ # example of running the releaser agains Dalston.SR1 tag with 1.0.0.BUILD-SNAPSHOT version of the releaser +$ java -jar ~/repo/spring-cloud-release-tools/spring-cloud-release-tools-spring/target/spring-cloud-release-tools-spring-1.0.0.BUILD-SNAPSHOT.jar --releaser.pom.branch=vDalston.SR1 --spring.config.name=releaser --full-release +---- + +.Doing the full release in non interactive mode (automatic release) +[source,bash] +---- +$ java -jar ~/repo/spring-cloud-release-tools/spring-cloud-release-tools-spring/target/spring-cloud-release-tools-spring-1.0.0.BUILD-SNAPSHOT.jar --releaser.pom.branch=vDalston.SR1 --spring.config.name=releaser --full-release --interactive=false +---- + +.Updating pom, closing milestone & createTemplates in interactive mode +[source,bash] +---- +$ java -jar ~/repo/spring-cloud-release-tools/spring-cloud-release-tools-spring/target/spring-cloud-release-tools-spring-1.0.0.BUILD-SNAPSHOT.jar --releaser.pom.branch=vDalston.SR1 --spring.config.name=releaser -u -m -t +---- + +.Running all tasks starting from 'push' (automatic) +[source,bash] +---- +$ java -jar ~/repo/spring-cloud-release-tools/spring-cloud-release-tools-spring/target/spring-cloud-release-tools-spring-1.0.0.BUILD-SNAPSHOT.jar --releaser.pom.branch=vDalston.SR1 --spring.config.name=releaser -a push -i=false +---- + +.Running tasks from 'docs' (inclusive) to 'push' (inclusive) (automatic) +[source,bash] +---- +$ java -jar ~/repo/spring-cloud-release-tools/spring-cloud-release-tools-spring/target/spring-cloud-release-tools-spring-1.0.0.BUILD-SNAPSHOT.jar --releaser.pom.branch=vDalston.SR1 --spring.config.name=releaser -r d-p -i=false +---- + +.Running single task 'closeMilestone' (automatic) +[source,bash] +---- +$ java -jar ~/repo/spring-cloud-release-tools/spring-cloud-release-tools-spring/target/spring-cloud-release-tools-spring-1.0.0.BUILD-SNAPSHOT.jar --releaser.pom.branch=vDalston.SR1 --spring.config.name=releaser --closeMilestone -i=false +---- + === Project options - `releaser.fixed-versions` - A String to String mapping of manually set versions. E.g. `"spring-cloud-cli" -> "1.0.0.RELEASE"` will set diff --git a/docs/src/main/asciidoc/spring-cloud-release-tools.adoc b/docs/src/main/asciidoc/spring-cloud-release-tools.adoc index fb30bae4..35fab155 100644 --- a/docs/src/main/asciidoc/spring-cloud-release-tools.adoc +++ b/docs/src/main/asciidoc/spring-cloud-release-tools.adoc @@ -31,7 +31,7 @@ why this tool makes it easy to automate the release / dependency update process - Generates a tweet template under `target/tweet.txt` (ONLY FOR NON-SNAPSHOT VERSIONS) - Generates a release notes template under `target/notes.md` (ONLY FOR NON-SNAPSHOT VERSIONS) -=== How to run it +=== What should I do first? Members of the Spring Cloud Team typically use this tool as follows. They first clone the releaser locally and build the jar manually @@ -43,6 +43,8 @@ $ cd spring-cloud-release-tools $ ./mvnw clean install ---- +=== How to run it (interactive mode) + Go to your project (e.g. Spring Cloud Sleuth) [source,bash] @@ -69,21 +71,133 @@ You will see text similar to this one 2) Update poms with versions from Spring Cloud Release 3) Build the project 4) Commit, tag and push the tag -5) Deploy the artifacts to Artifactory +5) Deploy the artifacts 6) Publish the docs 7) Go back to snapshots and bump originalVersion by patch 8) Push the commits 9) Close the milestone at Github -10) Create email / tweet etc. templates +10) Create email / blog / tweet etc. templates +You can pick a range of options by using the hyphen - e.g. '2-4' will execute jobs [2,3,4] +You can execute all tasks starting from a job by using a hyphen and providing only one number - e.g. '8-' will execute jobs [8,9,10] +You can execute given tasks by providing a colon separated list of tasks - e.g. '3,7,8' will execute jobs [3,7,8] You can press 'q' to quit ---- -Just pick a number and continue! Pick either a full release or single steps. +Just pick a number and continue! Pick either a full release or single steps. You can also pick +ranges or multiple steps. You can also provide the range only with the starting step +- that you will execute all steps starting from the given one. TIP: Read before picking a number cause it might have changed between tool releases ;) +=== How to run it (automatic mode) + +Go to your project (e.g. Spring Cloud Sleuth) and execute the application with `-h` or `--help` +flag. + +[source,bash] +---- +$ git clone git@github.com:spring-cloud/spring-cloud-sleuth.git +$ cd spring-cloud-sleuth +$ # example of running the releaser agains Dalston.SR1 tag with 1.0.0.BUILD-SNAPSHOT version of the releaser +$ java -jar ~/repo/spring-cloud-release-tools/spring-cloud-release-tools-spring/target/spring-cloud-release-tools-spring-1.0.0.BUILD-SNAPSHOT.jar --releaser.pom.branch=vDalston.SR1 --spring.config.name=releaser -h +---- + +You will see a help screen looking like more or less like this + +[source,bash] +---- +Here you can find the list of tasks in order + +[updatePoms,build,commit,deploy,docs,snapshots,push,closeMilestone,createTemplates] + + +Option Description +------ ----------- +-a, --start-from Starts all release task starting from the given + task. Requires passing the task name (either + one letter or the full name) +-b, --build [String] Build the project +-c, --commit [String] Commit, tag and push the tag +-d, --deploy [String] Deploy the artifacts +-f, --full-release [Boolean] Do you want to do the full release (default: + false) +-h, --help [String] +-i, --interactive Do you want to set the properties from the + command line (default: true) +-m, --closeMilestone [String] Close the milestone at Github +-o, --docs [String] Publish the docs +-p, --push [String] Push the commits +-r, --range Runs release tasks from the given range. + Requires passing the task names with a + hyphen. The first task is inclusive, the + second inclusive. E.g. 's-m' would mean + running 'snapshot', 'push' and 'milestone' + tasks +-s, --snapshots [String] Go back to snapshots and bump originalVersion + by patch +-t, --createTemplates [String] Create email / blog / tweet etc. templates +-u, --updatePoms [String] Update poms with versions from Spring Cloud + Release + +Examples of usage: + +Run 'build' & 'commit' & 'deploy' +java -jar releaser.jar -b -c -d + +Start from 'push' +java -jar releaser.jar -a push + +Range 'docs' -> 'push' +java -jar releaser.jar -r o-p +---- + +The Releaser can use two sets of options. The configuration options like `releaser.pom.branch` +and the task switches. For the tasks you can use either the full names or short switches. For example + providing range of tasks via switches `o-p` is equivalent to full name `docs-push`. + +A couple of examples: + +.Doing the full release in interactive mode (asking for skipping steps) +[source,bash] +---- +$ git clone git@github.com:spring-cloud/spring-cloud-sleuth.git +$ cd spring-cloud-sleuth +$ # example of running the releaser agains Dalston.SR1 tag with 1.0.0.BUILD-SNAPSHOT version of the releaser +$ java -jar ~/repo/spring-cloud-release-tools/spring-cloud-release-tools-spring/target/spring-cloud-release-tools-spring-1.0.0.BUILD-SNAPSHOT.jar --releaser.pom.branch=vDalston.SR1 --spring.config.name=releaser --full-release +---- + +.Doing the full release in non interactive mode (automatic release) +[source,bash] +---- +$ java -jar ~/repo/spring-cloud-release-tools/spring-cloud-release-tools-spring/target/spring-cloud-release-tools-spring-1.0.0.BUILD-SNAPSHOT.jar --releaser.pom.branch=vDalston.SR1 --spring.config.name=releaser --full-release --interactive=false +---- + +.Updating pom, closing milestone & createTemplates in interactive mode +[source,bash] +---- +$ java -jar ~/repo/spring-cloud-release-tools/spring-cloud-release-tools-spring/target/spring-cloud-release-tools-spring-1.0.0.BUILD-SNAPSHOT.jar --releaser.pom.branch=vDalston.SR1 --spring.config.name=releaser -u -m -t +---- + +.Running all tasks starting from 'push' (automatic) +[source,bash] +---- +$ java -jar ~/repo/spring-cloud-release-tools/spring-cloud-release-tools-spring/target/spring-cloud-release-tools-spring-1.0.0.BUILD-SNAPSHOT.jar --releaser.pom.branch=vDalston.SR1 --spring.config.name=releaser -a push -i=false +---- + +.Running tasks from 'docs' (inclusive) to 'push' (inclusive) (automatic) +[source,bash] +---- +$ java -jar ~/repo/spring-cloud-release-tools/spring-cloud-release-tools-spring/target/spring-cloud-release-tools-spring-1.0.0.BUILD-SNAPSHOT.jar --releaser.pom.branch=vDalston.SR1 --spring.config.name=releaser -r d-p -i=false +---- + +.Running single task 'closeMilestone' (automatic) +[source,bash] +---- +$ java -jar ~/repo/spring-cloud-release-tools/spring-cloud-release-tools-spring/target/spring-cloud-release-tools-spring-1.0.0.BUILD-SNAPSHOT.jar --releaser.pom.branch=vDalston.SR1 --spring.config.name=releaser --closeMilestone -i=false +---- + === Project options - `releaser.fixed-versions` - A String to String mapping of manually set versions. E.g. `"spring-cloud-cli" -> "1.0.0.RELEASE"` will set diff --git a/spring-cloud-release-tools-spring/pom.xml b/spring-cloud-release-tools-spring/pom.xml index 892f363b..e4da51f6 100644 --- a/spring-cloud-release-tools-spring/pom.xml +++ b/spring-cloud-release-tools-spring/pom.xml @@ -26,6 +26,12 @@ spring-cloud-release-tools-core ${project.version} + + net.sf.jopt-simple + jopt-simple + 5.0.3 + true + org.springframework.boot diff --git a/spring-cloud-release-tools-spring/src/main/java/org/springframework/cloud/release/internal/ReleaserApplication.java b/spring-cloud-release-tools-spring/src/main/java/org/springframework/cloud/release/internal/ReleaserApplication.java index f06d6c71..e5e15a5a 100644 --- a/spring-cloud-release-tools-spring/src/main/java/org/springframework/cloud/release/internal/ReleaserApplication.java +++ b/spring-cloud-release-tools-spring/src/main/java/org/springframework/cloud/release/internal/ReleaserApplication.java @@ -19,6 +19,8 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.CommandLineRunner; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.cloud.release.internal.options.Options; +import org.springframework.cloud.release.internal.options.Parser; import org.springframework.cloud.release.internal.spring.SpringReleaser; @SpringBootApplication @@ -29,9 +31,12 @@ public class ReleaserApplication implements CommandLineRunner { } @Autowired SpringReleaser releaser; + @Autowired Parser parser; @Override public void run(String... strings) throws Exception { - this.releaser.release(); + Options options = this.parser.parse(strings); + this.releaser.release(options); System.exit(0); } } + diff --git a/spring-cloud-release-tools-spring/src/main/java/org/springframework/cloud/release/internal/options/Options.java b/spring-cloud-release-tools-spring/src/main/java/org/springframework/cloud/release/internal/options/Options.java new file mode 100644 index 00000000..f352eefb --- /dev/null +++ b/spring-cloud-release-tools-spring/src/main/java/org/springframework/cloud/release/internal/options/Options.java @@ -0,0 +1,24 @@ +package org.springframework.cloud.release.internal.options; + +import java.util.ArrayList; +import java.util.List; + +/** + * @author Marcin Grzejszczak + */ +public class Options { + public Boolean fullRelease = true; + public Boolean interactive = true; + public List taskNames = new ArrayList<>(); + public String startFrom = ""; + public String range = ""; + + Options(Boolean fullRelease, Boolean interactive, List taskNames, String startFrom, + String range) { + this.fullRelease = fullRelease; + this.interactive = interactive; + this.taskNames = taskNames; + this.startFrom = startFrom; + this.range = range; + } +} diff --git a/spring-cloud-release-tools-spring/src/main/java/org/springframework/cloud/release/internal/options/OptionsBuilder.java b/spring-cloud-release-tools-spring/src/main/java/org/springframework/cloud/release/internal/options/OptionsBuilder.java new file mode 100644 index 00000000..0c018fc6 --- /dev/null +++ b/spring-cloud-release-tools-spring/src/main/java/org/springframework/cloud/release/internal/options/OptionsBuilder.java @@ -0,0 +1,42 @@ +package org.springframework.cloud.release.internal.options; + +import java.util.ArrayList; +import java.util.List; + +public class OptionsBuilder { + private Boolean fullRelease = false; + private Boolean interactive = true; + private List taskNames = new ArrayList<>(); + private String startFrom = ""; + private String range = ""; + + public OptionsBuilder fullRelease(Boolean fullRelease) { + this.fullRelease = fullRelease; + return this; + } + + public OptionsBuilder interactive(Boolean interactive) { + this.interactive = interactive; + return this; + } + + public OptionsBuilder taskNames(List taskNames) { + this.taskNames = taskNames; + return this; + } + + public OptionsBuilder startFrom(String startFrom) { + this.startFrom = startFrom; + return this; + } + + public OptionsBuilder range(String range) { + this.range = range; + return this; + } + + public Options options() { + return new Options(this.fullRelease, this.interactive, this.taskNames, this.startFrom, + this.range); + } +} \ No newline at end of file diff --git a/spring-cloud-release-tools-spring/src/main/java/org/springframework/cloud/release/internal/options/Parser.java b/spring-cloud-release-tools-spring/src/main/java/org/springframework/cloud/release/internal/options/Parser.java new file mode 100644 index 00000000..e3bf72b5 --- /dev/null +++ b/spring-cloud-release-tools-spring/src/main/java/org/springframework/cloud/release/internal/options/Parser.java @@ -0,0 +1,10 @@ +package org.springframework.cloud.release.internal.options; + +/** + * Converts input arguments to a {@link Options} + * + * @author Marcin Grzejszczak + */ +public interface Parser { + Options parse(String[] args); +} diff --git a/spring-cloud-release-tools-spring/src/main/java/org/springframework/cloud/release/internal/spring/Args.java b/spring-cloud-release-tools-spring/src/main/java/org/springframework/cloud/release/internal/spring/Args.java new file mode 100644 index 00000000..3e3b4274 --- /dev/null +++ b/spring-cloud-release-tools-spring/src/main/java/org/springframework/cloud/release/internal/spring/Args.java @@ -0,0 +1,33 @@ +package org.springframework.cloud.release.internal.spring; + +import java.io.File; + +import org.springframework.cloud.release.internal.Releaser; +import org.springframework.cloud.release.internal.ReleaserProperties; +import org.springframework.cloud.release.internal.pom.ProjectVersion; +import org.springframework.cloud.release.internal.pom.Projects; + +/** + * @author Marcin Grzejszczak + */ +class Args { + final Releaser releaser; + final File project; + final Projects projects; + final ProjectVersion originalVersion; + final ProjectVersion versionFromScRelease; + final ReleaserProperties properties; + final boolean interactive; + + Args(Releaser releaser, File project, Projects projects, ProjectVersion originalVersion, + ProjectVersion versionFromScRelease, ReleaserProperties properties, + boolean interactive) { + this.releaser = releaser; + this.project = project; + this.projects = projects; + this.originalVersion = originalVersion; + this.versionFromScRelease = versionFromScRelease; + this.properties = properties; + this.interactive = interactive; + } +} diff --git a/spring-cloud-release-tools-spring/src/main/java/org/springframework/cloud/release/internal/spring/OptionsParser.java b/spring-cloud-release-tools-spring/src/main/java/org/springframework/cloud/release/internal/spring/OptionsParser.java new file mode 100644 index 00000000..01b88cdb --- /dev/null +++ b/spring-cloud-release-tools-spring/src/main/java/org/springframework/cloud/release/internal/spring/OptionsParser.java @@ -0,0 +1,110 @@ +package org.springframework.cloud.release.internal.spring; + +import joptsimple.ArgumentAcceptingOptionSpec; +import joptsimple.OptionParser; +import joptsimple.OptionSet; + +import java.io.IOException; +import java.util.Arrays; +import java.util.List; +import java.util.stream.Collectors; + +import org.springframework.cloud.release.internal.options.Options; +import org.springframework.cloud.release.internal.options.OptionsBuilder; +import org.springframework.cloud.release.internal.options.Parser; + +/** + * @author Marcin Grzejszczak + */ +class OptionsParser implements Parser { + + @Override + public Options parse(String[] args) { + OptionParser parser = new OptionParser(); + parser.allowsUnrecognizedOptions(); + try { + ArgumentAcceptingOptionSpec fullReleaseOpt = parser + .acceptsAll(Arrays.asList("f", "full-release"), + "Do you want to do the full release") + .withOptionalArg().ofType(Boolean.class).defaultsTo(false); + ArgumentAcceptingOptionSpec interactiveOpt = parser + .acceptsAll(Arrays.asList("i", "interactive"), + "Do you want to set the properties from the command line") + .withRequiredArg().ofType(Boolean.class).defaultsTo(true); + Tasks.ALL_TASKS.forEach(task -> + parser.acceptsAll(Arrays.asList(task.shortName, task.name), + task.description) + .withOptionalArg()); + ArgumentAcceptingOptionSpec startFromOpt = parser + .acceptsAll(Arrays.asList("a", "start-from"), + "Starts all release task starting from the given task. Requires passing the task name (either one letter or the full name)") + .withRequiredArg().ofType(String.class); + ArgumentAcceptingOptionSpec rangeOpt = parser.acceptsAll(Arrays.asList("r", "range"), + "Runs release tasks from the given range. Requires passing the task names with a hyphen. The first task is inclusive, the second inclusive. E.g. 's-m' would mean running 'snapshot', 'push' and 'milestone' tasks") + .withRequiredArg().ofType(String.class); + parser.acceptsAll(Arrays.asList("h", "help")) + .withOptionalArg(); + OptionSet options = parser.parse(args); + if (options.has("h")) { + printHelpMessage(parser); + System.exit(0); + } + Boolean interactive = options.valueOf(interactiveOpt); + Boolean fullRelease = options.has(fullReleaseOpt); + List taskNames = Tasks.ALL_TASKS.stream() + .filter(task -> options.has(task.name)).map(task -> task.name) + .collect(Collectors.toList()); + String startFrom = options.valueOf(startFromOpt); + String range = options.valueOf(rangeOpt); + return new OptionsBuilder() + .fullRelease(fullRelease) + .interactive(interactive) + .taskNames(taskNames) + .startFrom(startFrom) + .range(range) + .options(); + } + catch (Exception e) { + printErrorMessage(e, parser); + throw e; + } + } + + private void printErrorMessage(Exception e, OptionParser parser) { + System.err.println(e.getMessage()); + System.err.println(intro()); + System.err.println( + "java -jar spring-cloud-release-tools-spring-1.0.0.BUILD-SNAPSHOT.jar [options...] "); + try { + parser.printHelpOn(System.err); + } catch (IOException e1) { + throw new IllegalStateException(e1); + } + System.err.println(examples()); + } + + private void printHelpMessage(OptionParser parser) { + try { + System.out.println(intro()); + parser.printHelpOn(System.out); + System.out.println(examples()); + } catch (IOException e1) { + throw new IllegalStateException(e1); + } + } + + private String intro() { + return "\nHere you can find the list of tasks in order\n\n[" + Tasks.tasksInOrder() + "]\n\n"; + } + + private String examples() { + return "\nExamples of usage:\n\n" + + "Run 'build' & 'commit' & 'deploy'\n" + + "java -jar jar.jar -b -c -d\n\n" + + "Start from 'push'\n" + + "java -jar releaser.jar -a push\n\n" + + "Range 'docs' -> 'push'\n" + + "java -jar releaser.jar -r o-p\n\n" + + "\n\n"; + } +} diff --git a/spring-cloud-release-tools-spring/src/main/java/org/springframework/cloud/release/internal/spring/OptionsProcessor.java b/spring-cloud-release-tools-spring/src/main/java/org/springframework/cloud/release/internal/spring/OptionsProcessor.java new file mode 100644 index 00000000..c76632e9 --- /dev/null +++ b/spring-cloud-release-tools-spring/src/main/java/org/springframework/cloud/release/internal/spring/OptionsProcessor.java @@ -0,0 +1,180 @@ +package org.springframework.cloud.release.internal.spring; + +import java.lang.invoke.MethodHandles; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.cloud.release.internal.Releaser; +import org.springframework.cloud.release.internal.ReleaserProperties; +import org.springframework.cloud.release.internal.options.Options; +import org.springframework.util.StringUtils; + +/** + * @author Marcin Grzejszczak + */ +class OptionsProcessor { + private static final Logger log = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass()); + + private final Releaser releaser; + private final ReleaserProperties properties; + private final List allTasks; + + OptionsProcessor(Releaser releaser, ReleaserProperties properties) { + this(releaser, properties, Tasks.ALL_TASKS); + } + + OptionsProcessor(Releaser releaser, ReleaserProperties properties, List allTasks) { + this.releaser = releaser; + this.properties = properties; + this.allTasks = allTasks; + } + + void processOptions(Options options, Args defaultArgs) { + Args args = args(defaultArgs, options.interactive); + if (options.fullRelease && !options.interactive) { + log.info("Executing a full release in non-interactive mode"); + releaseTask().execute(args); + } else if (options.fullRelease && options.interactive) { + log.info("Executing a full release in interactive mode"); + releaseVerboseTask().execute(args); + } else if (StringUtils.hasText(options.startFrom)) { + startFrom(options, args); + } else if (StringUtils.hasText(options.range)) { + range(options.range, args); + } else if (!options.taskNames.isEmpty()) { + tasks(options.taskNames, args); + } else if (options.interactive) { + interactiveOnly(args); + } else { + throw new IllegalStateException("You haven't picked any recognizable option"); + } + } + + Task releaseTask() { + return Tasks.RELEASE; + } + + Task releaseVerboseTask() { + return Tasks.RELEASE_VERBOSE; + } + + private void interactiveOnly(Args defaultArgs) { + log.info(buildOptionsText().toString()); + executeTaskFromOption(defaultArgs); + } + + private void tasks(List taskNames, Args defaultArgs) { + Tasks.forNames(this.allTasks, taskNames).forEach(task -> task.execute(defaultArgs)); + } + + private void range(String range, Args defaultArgs) { + String[] splitRange = range.split("-"); + String start = splitRange[0]; + String stop = ""; + if (splitRange.length == 2) { + stop = splitRange[1]; + } + boolean started = false; + boolean sameRange = start.equals(stop); + for (Task task : this.allTasks) { + if (start.equals(task.name) || start.equals(task.shortName)) { + started = true; + task.execute(defaultArgs); + if (sameRange) { + break; + } + } else if (started && (stop.equals(task.name) || stop.equals(task.shortName))) { + task.execute(defaultArgs); + break; + } else if (started) { + task.execute(defaultArgs); + } + } + } + + private void startFrom(Options options, Args defaultArgs) { + boolean started = false; + for (Task task : this.allTasks) { + if (options.startFrom.equals(task.name) || options.startFrom.equals(task.shortName)) { + started = true; + task.execute(defaultArgs); + } else if (started) { + task.execute(defaultArgs); + } + } + } + + private StringBuilder buildOptionsText() { + StringBuilder msg = new StringBuilder(); + msg.append("\n\n\n=== WHAT DO YOU WANT TO DO? ===\n\n"); + for (int i = 0; i < this.allTasks.size(); i++) { + msg.append(i).append(") ").append(this.allTasks.get(i).description).append("\n"); + } + msg.append("\n").append("You can pick a range of options by using the hyphen - e.g. '2-4' will execute jobs [2,3,4]\n"); + msg.append("You can execute all tasks starting from a job by using a hyphen and providing only one number - e.g. '8-' will execute jobs [8,9,10]\n"); + msg.append("You can execute given tasks by providing a colon separated list of tasks - e.g. '3,7,8' will execute jobs [3,7,8]\n"); + msg.append("\n").append("You can press 'q' to quit\n\n"); + return msg; + } + + void executeTaskFromOption(Args defaultArgs) { + String input = chosenOption(); + switch (input.toLowerCase()) { + case "q": + System.exit(0); + default: + if (input.contains("-")) { + rangeInteractive(defaultArgs, input); + } else if (input.contains(",")) { + tasksInteractive(defaultArgs, input); + } else { + singleTask(defaultArgs, input); + } + } + } + + private void singleTask(Args defaultArgs, String input) { + int chosenOption = Integer.parseInt(input); + Task task = this.allTasks.get(chosenOption); + boolean interactive = false; + if (task == Tasks.RELEASE_VERBOSE) { + interactive = true; + } + log.info("\n\n\nYou chose [{}]: [{}]\n\n\n", chosenOption, task.description); + task.execute(args(defaultArgs, interactive)); + } + + private void tasksInteractive(Args defaultArgs, String input) { + List tasks = Arrays.asList(input.split(",")); + List taskNames = new ArrayList<>(); + for (String task : tasks) { + Integer taskIndex = Integer.valueOf(task); + taskNames.add(this.allTasks.get(taskIndex).name); + } + tasks(taskNames, defaultArgs); + } + + private void rangeInteractive(Args defaultArgs, String input) { + String[] range = input.split("-"); + Integer start = Integer.valueOf(range[0]); + Integer stop = null; + if (range.length == 2) { + stop = Integer.valueOf(range[1]); + } + String firstName = this.allTasks.get(start).name; + String second = stop != null ? this.allTasks.get(stop).name : ""; + range(firstName + "-" + second, defaultArgs); + } + + private Args args(Args defaultArgs, boolean interactive) { + return new Args(this.releaser, defaultArgs.project, defaultArgs.projects, + defaultArgs.originalVersion, defaultArgs.versionFromScRelease, this.properties, interactive); + } + + String chosenOption() { + return System.console().readLine(); + } +} diff --git a/spring-cloud-release-tools-spring/src/main/java/org/springframework/cloud/release/internal/spring/ReleaserConfiguration.java b/spring-cloud-release-tools-spring/src/main/java/org/springframework/cloud/release/internal/spring/ReleaserConfiguration.java index cfd50d70..f0ecd9b3 100644 --- a/spring-cloud-release-tools-spring/src/main/java/org/springframework/cloud/release/internal/spring/ReleaserConfiguration.java +++ b/spring-cloud-release-tools-spring/src/main/java/org/springframework/cloud/release/internal/spring/ReleaserConfiguration.java @@ -19,6 +19,7 @@ import org.springframework.boot.context.properties.EnableConfigurationProperties import org.springframework.cloud.release.internal.Releaser; import org.springframework.cloud.release.internal.ReleaserProperties; import org.springframework.cloud.release.internal.gradle.GradleUpdater; +import org.springframework.cloud.release.internal.options.Parser; import org.springframework.cloud.release.internal.template.TemplateGenerator; import org.springframework.cloud.release.internal.project.ProjectBuilder; import org.springframework.cloud.release.internal.git.ProjectGitHandler; @@ -37,4 +38,8 @@ class ReleaserConfiguration { handler, new TemplateGenerator(properties, handler), new GradleUpdater(properties)), properties); } + + @Bean Parser optionsParser() { + return new OptionsParser(); + } } diff --git a/spring-cloud-release-tools-spring/src/main/java/org/springframework/cloud/release/internal/spring/SpringReleaser.java b/spring-cloud-release-tools-spring/src/main/java/org/springframework/cloud/release/internal/spring/SpringReleaser.java index a830bb8b..bbe1a7a4 100644 --- a/spring-cloud-release-tools-spring/src/main/java/org/springframework/cloud/release/internal/spring/SpringReleaser.java +++ b/spring-cloud-release-tools-spring/src/main/java/org/springframework/cloud/release/internal/spring/SpringReleaser.java @@ -3,19 +3,17 @@ package org.springframework.cloud.release.internal.spring; import java.io.File; import java.lang.invoke.MethodHandles; import java.util.List; -import java.util.function.Consumer; -import java.util.stream.Collectors; -import java.util.stream.Stream; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; import org.springframework.cloud.release.internal.Releaser; import org.springframework.cloud.release.internal.ReleaserProperties; +import org.springframework.cloud.release.internal.options.Options; +import org.springframework.cloud.release.internal.options.OptionsBuilder; import org.springframework.cloud.release.internal.pom.ProjectVersion; import org.springframework.cloud.release.internal.pom.Projects; -import static org.springframework.cloud.release.internal.spring.Task.task; - /** * Releaser that gets input from console * @@ -26,77 +24,45 @@ public class SpringReleaser { private final Releaser releaser; private final ReleaserProperties properties; + private final OptionsProcessor optionsProcessor; + @Autowired public SpringReleaser(Releaser releaser, ReleaserProperties properties) { this.releaser = releaser; this.properties = properties; + this.optionsProcessor = new OptionsProcessor(releaser, properties); } - private final List TASKS = Stream.of( - task("UPDATING POMS", - "Update poms with versions from Spring Cloud Release", - args -> args.releaser.updateProjectFromScRelease(args.project, args.projects, args.versionFromScRelease)), - task("BUILD PROJECT", - "Build the project", - args -> args.releaser.buildProject()), - task("COMMITTING (ALL) AND PUSHING TAGS (NON-SNAPSHOTS)", - "Commit, tag and push the tag", - args -> args.releaser.commitAndPushTags(args.project, args.versionFromScRelease)), - task("ARTIFACT DEPLOYMENT", - "Deploy the artifacts", - args -> args.releaser.deploy()), - task("PUBLISHING DOCS", - "Publish the docs", - args -> args.releaser.publishDocs(args.versionFromScRelease)), - task("REVERTING CHANGES & BUMPING VERSION (RELEASE ONLY)", - "Go back to snapshots and bump originalVersion by patch", - args -> args.releaser.rollbackReleaseVersion(args.project, args.versionFromScRelease)), - task("PUSHING CHANGES", - "Push the commits", - args -> args.releaser.pushCurrentBranch(args.project)), - task("CLOSING MILESTONE", - "Close the milestone at Github", - args -> args.releaser.closeMilestone(args.versionFromScRelease)), - task("CREATING TEMPLATES", - "Create email / blog / tweet etc. templates", - args -> { - args.releaser.createEmail(args.versionFromScRelease); - args.releaser.createBlog(args.versionFromScRelease, args.projects); - args.releaser.createTweet(args.versionFromScRelease); - args.releaser.createReleaseNotes(args.versionFromScRelease, args.projects); - }) - ).collect(Collectors.toList()); - - private final List COMPOSITE_TASKS = Stream.of( - task("FULL RELEASE", - "Perform a full release of this project without interruptions", - args -> TASKS.forEach(task -> task.execute(args))), - task("FULL VERBOSE RELEASE", - "Perform a full release of this project in a verbose mode (you'll be asked about skipping steps)", - args -> TASKS.forEach(task -> task.execute(args))) - ).collect(Collectors.toList()); - - private final List ALL_TASKS = Stream.of( - COMPOSITE_TASKS, - TASKS - ).flatMap(List::stream).collect(Collectors.toList()); + SpringReleaser(Releaser releaser, ReleaserProperties properties, + OptionsProcessor optionsProcessor) { + this.releaser = releaser; + this.properties = properties; + this.optionsProcessor = optionsProcessor; + } + /** + * Default behaviour - interactive mode + */ public void release() { - printVersionRetreival(); + release(new OptionsBuilder().options()); + } + + public void release(Options options) { + printVersionRetrieval(); String workingDir = this.properties.getWorkingDir(); File project = new File(workingDir); ProjectVersion originalVersion = new ProjectVersion(project); - Projects projects = this.releaser.retrieveVersionsFromSCRelease(); - ProjectVersion versionFromScRelease = projects.forFile(project); - assertNoSnapshotsForANonSnapshotProject(projects, versionFromScRelease); - log.info(buildOptionsText().toString()); - int chosenOption = chosenOption(); - log.info("\n\n\nYou chose [{}]: [{}]\n\n\n", chosenOption, ALL_TASKS.get(chosenOption).description); - boolean verbose = chosenOption == 1; - Task task = taskFromOption(chosenOption); - Args args = new Args(this.releaser, project, projects, originalVersion, versionFromScRelease, - this.properties, verbose); - task.consumer.accept(args); + Projects projectsFromScRelease = this.releaser.retrieveVersionsFromSCRelease(); + ProjectVersion versionFromScRelease = projectsFromScRelease.forFile(project); + assertNoSnapshotsForANonSnapshotProject(projectsFromScRelease, versionFromScRelease); + final Args defaultArgs = new Args(this.releaser, project, projectsFromScRelease, + originalVersion, versionFromScRelease, this.properties, options.interactive); + this.optionsProcessor.processOptions(options, defaultArgs); + } + + private void printVersionRetrieval() { + log.info("\n\n\n=== RETRIEVING VERSIONS ===\n\nWill clone Spring Cloud Release" + + " to retrieve all versions for the branch [{}]", this.properties.getPom().getBranch()); } private void assertNoSnapshotsForANonSnapshotProject(Projects projects, @@ -108,104 +74,6 @@ public class SpringReleaser { } } - private StringBuilder buildOptionsText() { - StringBuilder msg = new StringBuilder(); - msg.append("\n\n\n=== WHAT DO YOU WANT TO DO? ===\n\n"); - for (int i = 0; i < ALL_TASKS.size(); i++) { - msg.append(i).append(") ").append(ALL_TASKS.get(i).description).append("\n"); - } - msg.append("\n\n").append("You can press 'q' to quit\n\n"); - return msg; - } - private void printVersionRetreival() { - log.info("\n\n\n=== RETRIEVING VERSIONS ===\n\nWill clone Spring Cloud Release" - + " to retrieve all versions for the branch [{}]", this.properties.getPom().getBranch()); - } - - private Task taskFromOption(int option) { - return ALL_TASKS.get(option); - } - - int chosenOption() { - String input = System.console().readLine(); - switch (input.toLowerCase()) { - case "q": - System.exit(0); - default: - return Integer.parseInt(input); - } - } } -class Task { - - private static final Logger log = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass()); - private static final String MSG = "'q' to quit and 's' to skip\n\n"; - - final String header; - final String description; - final Consumer consumer; - - Task(String header, String description, Consumer consumer) { - this.header = header; - this.description = description; - this.consumer = consumer; - } - - void execute(Args args) { - boolean verbose = args.verbose; - printLog(verbose); - if (verbose) { - boolean skipStep = skipStep(); - if (!skipStep) { - consumer.accept(args); - } - } else { - consumer.accept(args); - } - } - - private void printLog(boolean shouldSkip) { - log.info("\n\n\n=== {} ===\n\n{} {}\n\n", header, description, shouldSkip ? MSG : ""); - } - - boolean skipStep() { - String input = System.console().readLine(); - switch (input.toLowerCase()) { - case "s": - return true; - case "q": - System.exit(0); - return true; - default: - return false; - } - } - - static Task task(String header, String description, Consumer function) { - return new Task(header, description, function); - } -} - -class Args { - final Releaser releaser; - final File project; - final Projects projects; - final ProjectVersion originalVersion; - final ProjectVersion versionFromScRelease; - final ReleaserProperties properties; - final boolean verbose; - - Args(Releaser releaser, File project, Projects projects, ProjectVersion originalVersion, - ProjectVersion versionFromScRelease, ReleaserProperties properties, - boolean verbose) { - this.releaser = releaser; - this.project = project; - this.projects = projects; - this.originalVersion = originalVersion; - this.versionFromScRelease = versionFromScRelease; - this.properties = properties; - this.verbose = verbose; - } -} \ No newline at end of file diff --git a/spring-cloud-release-tools-spring/src/main/java/org/springframework/cloud/release/internal/spring/Task.java b/spring-cloud-release-tools-spring/src/main/java/org/springframework/cloud/release/internal/spring/Task.java new file mode 100644 index 00000000..c64f05bd --- /dev/null +++ b/spring-cloud-release-tools-spring/src/main/java/org/springframework/cloud/release/internal/spring/Task.java @@ -0,0 +1,65 @@ +package org.springframework.cloud.release.internal.spring; + +import java.lang.invoke.MethodHandles; +import java.util.function.Consumer; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * @author Marcin Grzejszczak + */ +class Task { + + private static final Logger log = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass()); + private static final String MSG = "\nPress 'q' to quit, 's' to skip, any key to continue\n\n"; + + final String name; + final String shortName; + final String header; + final String description; + private final Consumer consumer; + + Task(String name, String shortName, String header, String description, Consumer consumer) { + this.name = name; + this.shortName = shortName; + this.header = header; + this.description = description; + this.consumer = consumer; + } + + void execute(Args args) { + boolean interactive = args.interactive; + printLog(interactive); + if (interactive) { + boolean skipStep = skipStep(); + if (!skipStep) { + consumer.accept(args); + } + } else { + consumer.accept(args); + } + } + + private void printLog(boolean interactive) { + log.info("\n\n\n=== {} ===\n\n{} {}\n\n", header, description, interactive ? MSG : ""); + } + + boolean skipStep() { + String input = chosenOption(); + switch (input.toLowerCase()) { + case "s": + return true; + case "q": + System.exit(0); + return true; + default: + return false; + } + } + + String chosenOption() { + return System.console().readLine(); + } + +} diff --git a/spring-cloud-release-tools-spring/src/main/java/org/springframework/cloud/release/internal/spring/Tasks.java b/spring-cloud-release-tools-spring/src/main/java/org/springframework/cloud/release/internal/spring/Tasks.java new file mode 100644 index 00000000..a6a686bc --- /dev/null +++ b/spring-cloud-release-tools-spring/src/main/java/org/springframework/cloud/release/internal/spring/Tasks.java @@ -0,0 +1,99 @@ +package org.springframework.cloud.release.internal.spring; + +import java.util.List; +import java.util.function.Consumer; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +/** + * All tasks that can be executed by the releaser + * + * @author Marcin Grzejszczak + */ +class Tasks { + static Task UPDATING_POMS = task("updatePoms", "u", + "UPDATING POMS", + "Update poms with versions from Spring Cloud Release", + args -> args.releaser.updateProjectFromScRelease(args.project, args.projects, args.versionFromScRelease)); + static Task BUILD_PROJECT = task("build", "b", + "BUILD PROJECT", + "Build the project", + args -> args.releaser.buildProject()); + static Task COMMIT = task("commit", "c", + "COMMITTING (ALL) AND PUSHING TAGS (NON-SNAPSHOTS)", + "Commit, tag and push the tag", + args -> args.releaser.commitAndPushTags(args.project, args.versionFromScRelease)); + static Task DEPLOY = task("deploy", "d", + "ARTIFACT DEPLOYMENT", + "Deploy the artifacts", + args -> args.releaser.deploy()); + static Task PUBLISH_DOCS = task("docs", "o", + "PUBLISHING DOCS", + "Publish the docs", + args -> args.releaser.publishDocs(args.versionFromScRelease)); + static Task SNAPSHOTS = task("snapshots", "s", + "REVERTING CHANGES & BUMPING VERSION (RELEASE ONLY)", + "Go back to snapshots and bump originalVersion by patch", + args -> args.releaser.rollbackReleaseVersion(args.project, args.versionFromScRelease)); + static Task PUSH = task("push", "p", + "PUSHING CHANGES", + "Push the commits", + args -> args.releaser.pushCurrentBranch(args.project)); + static Task CLOSE_MILESTONE = task("closeMilestone", "m", + "CLOSING MILESTONE", + "Close the milestone at Github", + args -> args.releaser.closeMilestone(args.versionFromScRelease)); + static Task CREATE_TEMPLATES = task("createTemplates", "t", + "CREATING TEMPLATES", + "Create email / blog / tweet etc. templates", + args -> { + args.releaser.createEmail(args.versionFromScRelease); + args.releaser.createBlog(args.versionFromScRelease, args.projects); + args.releaser.createTweet(args.versionFromScRelease); + args.releaser.createReleaseNotes(args.versionFromScRelease, args.projects); + }); + + static final List DEFAULT_TASKS = Stream.of( + Tasks.UPDATING_POMS, + Tasks.BUILD_PROJECT, + Tasks.COMMIT, + Tasks.DEPLOY, + Tasks.PUBLISH_DOCS, + Tasks.SNAPSHOTS, + Tasks.PUSH, + Tasks.CLOSE_MILESTONE, + Tasks.CREATE_TEMPLATES + ).collect(Collectors.toList()); + + static Task RELEASE = Tasks.task("release", "r", + "FULL RELEASE", + "Perform a full release of this project without interruptions", + args -> DEFAULT_TASKS.forEach(task -> task.execute(args))); + static Task RELEASE_VERBOSE = Tasks.task("release-verbose", "r", + "FULL VERBOSE RELEASE", + "Perform a full release of this project in interactive mode (you'll be asked about skipping steps)", + args -> DEFAULT_TASKS.forEach(task -> task.execute(args))); + + static final List COMPOSITE_TASKS = Stream.of( + RELEASE, + RELEASE_VERBOSE + ).collect(Collectors.toList()); + + static final List ALL_TASKS = Stream.of( + COMPOSITE_TASKS, DEFAULT_TASKS + ).flatMap(List::stream).collect(Collectors.toList()); + + static Task task(String name, String shortName, String header, String description, Consumer function) { + return new Task(name, shortName, header, description, function); + } + + static List forNames(List tasks, List names) { + return tasks.stream() + .filter(task -> names.contains(task.name) || names.contains(task.shortName)) + .collect(Collectors.toList()); + } + + static String tasksInOrder() { + return DEFAULT_TASKS.stream().map(task -> task.name).collect(Collectors.joining(",")); + } +} diff --git a/spring-cloud-release-tools-spring/src/test/java/org/springframework/cloud/release/internal/spring/AcceptanceTests.java b/spring-cloud-release-tools-spring/src/test/java/org/springframework/cloud/release/internal/spring/AcceptanceTests.java index cf6786d4..c6a7e28f 100644 --- a/spring-cloud-release-tools-spring/src/test/java/org/springframework/cloud/release/internal/spring/AcceptanceTests.java +++ b/spring-cloud-release-tools-spring/src/test/java/org/springframework/cloud/release/internal/spring/AcceptanceTests.java @@ -236,11 +236,11 @@ public class AcceptanceTests { private SpringReleaser releaserWithFullDeployment(String expectedVersion, ReleaserProperties properties) throws Exception { Releaser releaser = defaultReleaser(expectedVersion, properties); - return new SpringReleaser(releaser, properties) { - @Override int chosenOption() { - return 0; + return new SpringReleaser(releaser, properties, new OptionsProcessor(releaser, properties) { + @Override String chosenOption() { + return "0"; } - }; + }); } private SpringReleaser releaserWithSnapshotScRelease(File projectFile, String branch, String expectedVersion) throws Exception { @@ -251,11 +251,11 @@ public class AcceptanceTests { private SpringReleaser templateOnlyReleaser(File projectFile, String branch, String expectedVersion) throws Exception { ReleaserProperties properties = releaserProperties(projectFile, branch); Releaser releaser = defaultReleaser(expectedVersion, properties); - return new SpringReleaser(releaser, properties) { - @Override int chosenOption() { - return 10; + return new SpringReleaser(releaser, properties, new OptionsProcessor(releaser, properties) { + @Override String chosenOption() { + return "10"; } - }; + }); } private Releaser defaultReleaser(String expectedVersion, ReleaserProperties properties) throws Exception { diff --git a/spring-cloud-release-tools-spring/src/test/java/org/springframework/cloud/release/internal/spring/OptionsProcessorTests.java b/spring-cloud-release-tools-spring/src/test/java/org/springframework/cloud/release/internal/spring/OptionsProcessorTests.java new file mode 100644 index 00000000..39875baf --- /dev/null +++ b/spring-cloud-release-tools-spring/src/test/java/org/springframework/cloud/release/internal/spring/OptionsProcessorTests.java @@ -0,0 +1,290 @@ +package org.springframework.cloud.release.internal.spring; + +import edu.emory.mathcs.backport.java.util.Arrays; + +import java.util.List; +import java.util.function.Consumer; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.runners.MockitoJUnitRunner; +import org.springframework.cloud.release.internal.Releaser; +import org.springframework.cloud.release.internal.ReleaserProperties; +import org.springframework.cloud.release.internal.options.Options; +import org.springframework.cloud.release.internal.options.OptionsBuilder; + +import static org.assertj.core.api.BDDAssertions.then; +import static org.assertj.core.api.BDDAssertions.thenThrownBy; + +/** + * @author Marcin Grzejszczak + */ +@RunWith(MockitoJUnitRunner.class) +@SuppressWarnings("unchecked") +public class OptionsProcessorTests { + + @Mock Releaser releaser; + FirstConsumer first = new FirstConsumer(); + SecondConsumer second = new SecondConsumer(); + ThirdConsumer third = new ThirdConsumer(); + Task firstTask = task("first", "1", "", "", first); + List tasks = Arrays.asList(new Task[] { + firstTask, + task("second", "2", "", "", second), + task("third", "3", "", "", third) + }); + + OptionsProcessor optionsProcessor; + + @Before + public void setup() { + this.optionsProcessor = new OptionsProcessor(this.releaser, new ReleaserProperties(), this.tasks); + } + + @Test + public void should_throw_exception_when_an_invalid_option_was_picked() throws Exception { + Options options = nonInteractiveOpts().options(); + + thenThrownBy(() -> this.optionsProcessor.processOptions(options, args())) + .hasMessageContaining("You haven't picked any recognizable option"); + } + + @Test + public void should_execute_only_tasks_after_the_provided_one_using_full_name() throws Exception { + Options options = nonInteractiveOpts().startFrom("second").options(); + + this.optionsProcessor.processOptions(options, args()); + + then(this.first.executed).isFalse(); + then(this.second.executed).isTrue(); + then(this.third.executed).isTrue(); + } + + + @Test + public void should_execute_only_tasks_after_the_provided_one_using_short_name() throws Exception { + Options options = nonInteractiveOpts().startFrom("2").options(); + + this.optionsProcessor.processOptions(options, args()); + + then(this.first.executed).isFalse(); + then(this.second.executed).isTrue(); + then(this.third.executed).isTrue(); + } + + @Test + public void should_execute_only_tasks_from_range_using_full_name() throws Exception { + Options options = nonInteractiveOpts().range("second-third").options(); + + this.optionsProcessor.processOptions(options, args()); + + then(this.first.executed).isFalse(); + then(this.second.executed).isTrue(); + then(this.third.executed).isTrue(); + } + + @Test + public void should_execute_only_tasks_from_range_using_short_name() throws Exception { + Options options = nonInteractiveOpts().range("2-3").options(); + + this.optionsProcessor.processOptions(options, args()); + + then(this.first.executed).isFalse(); + then(this.second.executed).isTrue(); + then(this.third.executed).isTrue(); + } + + @Test + public void should_execute_only_tasks_from_range_using_full_name_with_same_range() throws Exception { + Options options = nonInteractiveOpts().range("second-second").options(); + + this.optionsProcessor.processOptions(options, args()); + + then(this.first.executed).isFalse(); + then(this.second.executed).isTrue(); + then(this.third.executed).isFalse(); + } + + @Test + public void should_execute_only_tasks_from_range_using_short_name_with_same_range() throws Exception { + Options options = nonInteractiveOpts().range("2-2").options(); + + this.optionsProcessor.processOptions(options, args()); + + then(this.first.executed).isFalse(); + then(this.second.executed).isTrue(); + then(this.third.executed).isFalse(); + } + + @Test + public void should_execute_only_tasks_from_multi_using_full_name() throws Exception { + Options options = nonInteractiveOpts().taskNames(list("first", "third")).options(); + + this.optionsProcessor.processOptions(options, args()); + + then(this.first.executed).isTrue(); + then(this.second.executed).isFalse(); + then(this.third.executed).isTrue(); + } + + @Test + public void should_execute_only_tasks_from_multi_using_short_name() throws Exception { + Options options = nonInteractiveOpts().taskNames(list("1", "3")).options(); + + this.optionsProcessor.processOptions(options, args()); + + then(this.first.executed).isTrue(); + then(this.second.executed).isFalse(); + then(this.third.executed).isTrue(); + } + + @Test + public void should_execute_interactively_only_single_task() throws Exception { + this.optionsProcessor = new OptionsProcessor(this.releaser, new ReleaserProperties(), this.tasks) { + @Override String chosenOption() { + return "0"; + } + }; + Options options = interactiveOpts().options(); + + this.optionsProcessor.processOptions(options, args()); + + then(this.first.executed).isTrue(); + then(this.second.executed).isFalse(); + then(this.third.executed).isFalse(); + } + + @Test + public void should_execute_interactively_range_of_tasks() throws Exception { + this.optionsProcessor = new OptionsProcessor(this.releaser, new ReleaserProperties(), this.tasks) { + @Override String chosenOption() { + return "0-1"; + } + }; + Options options = interactiveOpts().options(); + + this.optionsProcessor.processOptions(options, args()); + + then(this.first.executed).isTrue(); + then(this.second.executed).isTrue(); + then(this.third.executed).isFalse(); + } + + @Test + public void should_execute_interactively_start_from() throws Exception { + this.optionsProcessor = new OptionsProcessor(this.releaser, new ReleaserProperties(), this.tasks) { + @Override String chosenOption() { + return "1-"; + } + }; + Options options = interactiveOpts().options(); + + this.optionsProcessor.processOptions(options, args()); + + then(this.first.executed).isFalse(); + then(this.second.executed).isTrue(); + then(this.third.executed).isTrue(); + } + + @Test + public void should_execute_interactively_multi() throws Exception { + this.optionsProcessor = new OptionsProcessor(this.releaser, new ReleaserProperties(), this.tasks) { + @Override String chosenOption() { + return "0,2"; + } + }; + Options options = interactiveOpts().options(); + + this.optionsProcessor.processOptions(options, args()); + + then(this.first.executed).isTrue(); + then(this.second.executed).isFalse(); + then(this.third.executed).isTrue(); + } + + @Test + public void should_execute_full_release() throws Exception { + this.optionsProcessor = new OptionsProcessor(this.releaser, new ReleaserProperties(), this.tasks) { + @Override Task releaseTask() { + return firstTask; + } + }; + Options options = nonInteractiveOpts().fullRelease(true).options(); + + this.optionsProcessor.processOptions(options, args()); + + then(this.first.executed).isTrue(); + then(this.second.executed).isFalse(); + then(this.third.executed).isFalse(); + } + + @Test + public void should_execute_full_verbose_release() throws Exception { + this.optionsProcessor = new OptionsProcessor(this.releaser, new ReleaserProperties(), this.tasks) { + @Override Task releaseVerboseTask() { + return firstTask; + } + }; + Options options = interactiveOpts().fullRelease(true).options(); + + this.optionsProcessor.processOptions(options, args()); + + then(this.first.executed).isTrue(); + then(this.second.executed).isFalse(); + then(this.third.executed).isFalse(); + } + + private OptionsBuilder interactiveOpts() { + return new OptionsBuilder(); + } + + private OptionsBuilder nonInteractiveOpts() { + return new OptionsBuilder().interactive(false); + } + + private Args args() { + return new Args(null, null, null, null, null, null, false); + } + + private List list(String... list) { + return Arrays.asList(list); + } + + static Task task(String name, String shortName, String header, String description, Consumer function) { + return new Task(name, shortName, header, description, function) { + @Override String chosenOption() { + return "whatever"; + } + }; + } + +} + +class FirstConsumer implements Consumer { + + boolean executed; + + @Override public void accept(Args o) { + this.executed = true; + } +} + +class SecondConsumer implements Consumer { + + boolean executed; + + @Override public void accept(Args o) { + this.executed = true; + } +} + +class ThirdConsumer implements Consumer { + + boolean executed; + + @Override public void accept(Args o) { + this.executed = true; + } +} \ No newline at end of file