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