From 9578294fbd5246ac0cbb3842848df98da098cef9 Mon Sep 17 00:00:00 2001 From: Marcin Grzejszczak Date: Mon, 25 Jun 2018 09:21:24 +0200 Subject: [PATCH] Meta release (#84) ## How it works? - Uses the fixed versions to clone and check out each project (e.g. `spring-cloud-sleuth: 2.1.0.RELEASE`) - From the version analyzes the branch and checks it out. E.g. - for `spring-cloud-release`'s `Finchley.RELEASE` version will resolve either `Finchley.x` branch or will fallback to `master` if there's no `Finchley.x` branch. - for `spring-cloud-sleuth`'s `2.1.0.RELEASE` version will resolve `2.1.x` branch - Performs the release tasks per each project - Performs the post release tasks at the end of the release ## Required options - `releaser.fixed-versions` - A String to String mapping of manually set versions. E.g. `"spring-cloud-cli" -> "1.0.0.RELEASE"` will set the `spring-cloud-cli.version` to `1.0.0.RELEASE` regardless of what was set in `spring-cloud-release` project. Example `--releaser.fixed-versions[spring-cloud-cli]=1.0.0.RELEASE`. Use these properties to provide versions for the meta release. - `releaser.meta-release.enabled` - You have to turn it on to enable a meta release. Defaults to `false` - `releaser.meta-release.git-org-url` - The URL of the Git organization. We'll append each project's name to it. Defaults to `https://github.com/spring-cloud` run the task via `-x=true` --- README.adoc | 47 +++- .../asciidoc/spring-cloud-release-tools.adoc | 47 +++- pom.xml | 4 +- spring-cloud-release-tools-core/foo | 0 spring-cloud-release-tools-core/pom.xml | 1 + .../cloud/release/internal/Releaser.java | 4 + .../release/internal/ReleaserProperties.java | 56 +++++ .../internal/ReleaserPropertiesAware.java | 9 + .../cloud/release/internal/git/GitRepo.java | 48 ++-- .../internal/git/ProjectGitHandler.java | 80 ++++++- .../internal/gradle/GradleUpdater.java | 9 +- .../internal/pom/ProjectPomUpdater.java | 9 +- .../cloud/release/internal/pom/Projects.java | 5 + .../internal/pom/SCReleasePomParser.java | 8 +- .../internal/project/ProjectBuilder.java | 21 +- .../internal/template/TemplateGenerator.java | 9 +- .../release/internal/git/GitRepoTests.java | 72 +++--- .../internal/git/ProjectGitHandlerTests.java | 75 +++++-- .../release/internal/pom/ProjectsTests.java | 9 + .../internal/pom/PropertyStorerTests.java | 15 +- .../internal/pom/SCReleasePomParserTests.java | 9 + .../internal/sagan/SaganUpdaterTest.java | 17 +- spring-cloud-release-tools-spring/pom.xml | 4 + .../release/internal/ReleaserApplication.java | 3 +- .../release/internal/options/Options.java | 11 +- .../internal/options/OptionsBuilder.java | 9 +- .../cloud/release/internal/spring/Args.java | 32 ++- .../internal/spring/OptionsParser.java | 16 +- .../internal/spring/OptionsProcessor.java | 99 ++++++--- .../spring/ReleaserConfiguration.java | 42 +++- .../spring/ReleaserPropertiesUpdater.java | 24 ++ .../internal/spring/SpringReleaser.java | 148 ++++++++++++- .../cloud/release/internal/spring/Task.java | 18 +- .../cloud/release/internal/spring/Tasks.java | 64 ++++-- .../internal/spring/AcceptanceTests.java | 208 +++++++++++++++++- .../spring/OptionsProcessorTests.java | 10 +- .../ReleaserPropertiesIntegrationTests.java | 67 ++++++ .../ReleaserPropertiesUpdaterTests.java | 48 ++++ .../release/internal/spring/TaskTests.java | 13 +- 39 files changed, 1156 insertions(+), 214 deletions(-) create mode 100644 spring-cloud-release-tools-core/foo create mode 100644 spring-cloud-release-tools-core/src/main/java/org/springframework/cloud/release/internal/ReleaserPropertiesAware.java create mode 100644 spring-cloud-release-tools-spring/src/main/java/org/springframework/cloud/release/internal/spring/ReleaserPropertiesUpdater.java create mode 100644 spring-cloud-release-tools-spring/src/test/java/org/springframework/cloud/release/internal/spring/ReleaserPropertiesIntegrationTests.java create mode 100644 spring-cloud-release-tools-spring/src/test/java/org/springframework/cloud/release/internal/spring/ReleaserPropertiesUpdaterTests.java diff --git a/README.adoc b/README.adoc index 63c7b399..867db0d9 100644 --- a/README.adoc +++ b/README.adoc @@ -25,6 +25,10 @@ why this tool makes it easy to automate the release / dependency update process === What does it do? +==== Single project + +For a single project + - Clones the Spring Cloud Release project and picks all versions (Boot + Cloud projects) - Modifies the project versions with values from SC-Release * throws an exception when we bump versions to release and there's a SNAPSHOT version referenced in the POM @@ -36,6 +40,12 @@ why this tool makes it easy to automate the release / dependency update process - Publishes the docs (to `spring-cloud-static` for non-snapshots, to `gh-pages` for snapshots) - Reverts back to snapshots, bumps the version by a patch (`1.0.1.RELEASE` -> `1.0.2.BUILD-SNAPSHOT`) (ONLY FOR RELEASE VERSIONS) - Closes the milestone on Github (e.g. `v1.0.1.RELEASE`) (ONLY FOR NON-SNAPSHOT VERSIONS) + +IMPORTANT: Starting with version that does Sagan integration, you MUST pass the OAuth token, +otherwise the application will fail to start + +After project release + - Generates an email template under `target/email.txt` (ONLY FOR NON-SNAPSHOT VERSIONS) - Generates a blog template under `target/blog.md` (ONLY FOR NON-SNAPSHOT VERSIONS) - Generates a tweet template under `target/tweet.txt` (ONLY FOR NON-SNAPSHOT VERSIONS) @@ -44,8 +54,17 @@ why this tool makes it easy to automate the release / dependency update process - For `GA`/ `SR` release will create an issue in Spring Guides under https://github.com/spring-guides/getting-started-guides/issues/ - For `GA`/ `SR` release will update the links under https://github.com/spring-cloud/spring-cloud-static/tree/gh-pages/current -IMPORTANT: Starting with version that does Sagan integration, you MUST pass the OAuth token, -otherwise the application will fail to start +==== Meta-release + +- Uses the fixed versions to clone and check out each project (e.g. `spring-cloud-sleuth: 2.1.0.RELEASE`) +- From the version analyzes the branch and checks it out. E.g. +** for `spring-cloud-release`'s `Finchley.RELEASE` version will resolve either `Finchley.x` branch or will fallback to `master` if there's no `Finchley.x` branch. +** for `spring-cloud-sleuth`'s `2.1.0.RELEASE` version will resolve `2.1.x` branch +- Performs the release tasks per each project +- Performs the post release tasks at the end of the release + +IMPORTANT: For the meta-releaser to work we assume that the path to the +custom configuration file for each project is always `config/releaser.yml`. === What should I do first? @@ -225,10 +244,31 @@ $ java -jar ~/repo/spring-cloud-release-tools/spring-cloud-release-tools-spring/ $ 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 ---- +=== How to run meta-release (automatic-mode) + +All you have to do is run the jar with the releaser and pass the +`-x=true` option to turn on meta-release and a list of fixed versions +in the `--"releaser.fixed-versions[project-name]=project-version" format + +``` +$ java -jar spring-cloud-release-tools-spring/target/spring-cloud-release-tools-spring-1.0.0.BUILD-SNAPSHOT.jar --spring.config.name=releaser -x=true --"releaser.fixed-versions[spring-cloud-sleuth]=2.0.1.BUILD-SNAPSHOT" +``` + +IMPORTANT: For the meta release the `startFrom` or `taskNames` take into consideration +the project names, not task names. E.g. you can start from `spring-cloud-netflix` project, +or build only tasks with names `spring-cloud-build,spring-cloud-sleuth`. + === 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 the `spring-cloud-cli.version` to `1.0.0.RELEASE` regardless of what was set in `spring-cloud-release` project. Example `--releaser.fixed-versions[spring-cloud-cli]=1.0.0.RELEASE`. +Use these properties to provide versions for the meta release. + +- `releaser.meta-release.enabled` - You have to turn it on to enable a meta release. Defaults to `false` +- `releaser.meta-release.release-train-project-name` - Name of the project that represents the BOM of the release train. Defaults to `spring-cloud-release` +- `releaser.meta-release.git-org-url` - The URL of the Git organization. We'll append each project's name to it. +Defaults to `https://github.com/spring-cloud` + - `releaser.git.fetch-versions-from-git` - If `true` then should fill the map of versions from Git. If `false` then picks fixed versions - `releaser.git.clone-destination-dir` - Where should the Spring Cloud Release repo get cloned to. If null defaults to a temporary directory - `releaser.git.spring-cloud-release-git-url` - URL to Spring Cloud Release Git repository. Defaults to `https://github.com/spring-cloud/spring-cloud-release` @@ -282,6 +322,9 @@ $ java -jar target/spring-cloud-release-tools-spring-1.0.0.M1.jar --spring.confi TIP: Notice that we're downloading the jar to a parent folder, not to `target`. That's because `target` get cleaned during the build process +IMPORTANT: For the meta-releaser to work we assume that the path to the +configuration file is always `config/releaser.yml`. + ==== Specifying A Branch By deafult the releaser will default to using the `master` branch of `spring-cloud-release`. diff --git a/docs/src/main/asciidoc/spring-cloud-release-tools.adoc b/docs/src/main/asciidoc/spring-cloud-release-tools.adoc index 2472ef0e..05d524cc 100644 --- a/docs/src/main/asciidoc/spring-cloud-release-tools.adoc +++ b/docs/src/main/asciidoc/spring-cloud-release-tools.adoc @@ -15,6 +15,10 @@ why this tool makes it easy to automate the release / dependency update process === What does it do? +==== Single project + +For a single project + - Clones the Spring Cloud Release project and picks all versions (Boot + Cloud projects) - Modifies the project versions with values from SC-Release * throws an exception when we bump versions to release and there's a SNAPSHOT version referenced in the POM @@ -26,6 +30,12 @@ why this tool makes it easy to automate the release / dependency update process - Publishes the docs (to `spring-cloud-static` for non-snapshots, to `gh-pages` for snapshots) - Reverts back to snapshots, bumps the version by a patch (`1.0.1.RELEASE` -> `1.0.2.BUILD-SNAPSHOT`) (ONLY FOR RELEASE VERSIONS) - Closes the milestone on Github (e.g. `v1.0.1.RELEASE`) (ONLY FOR NON-SNAPSHOT VERSIONS) + +IMPORTANT: Starting with version that does Sagan integration, you MUST pass the OAuth token, +otherwise the application will fail to start + +After project release + - Generates an email template under `target/email.txt` (ONLY FOR NON-SNAPSHOT VERSIONS) - Generates a blog template under `target/blog.md` (ONLY FOR NON-SNAPSHOT VERSIONS) - Generates a tweet template under `target/tweet.txt` (ONLY FOR NON-SNAPSHOT VERSIONS) @@ -34,8 +44,17 @@ why this tool makes it easy to automate the release / dependency update process - For `GA`/ `SR` release will create an issue in Spring Guides under https://github.com/spring-guides/getting-started-guides/issues/ - For `GA`/ `SR` release will update the links under https://github.com/spring-cloud/spring-cloud-static/tree/gh-pages/current -IMPORTANT: Starting with version that does Sagan integration, you MUST pass the OAuth token, -otherwise the application will fail to start +==== Meta-release + +- Uses the fixed versions to clone and check out each project (e.g. `spring-cloud-sleuth: 2.1.0.RELEASE`) +- From the version analyzes the branch and checks it out. E.g. +** for `spring-cloud-release`'s `Finchley.RELEASE` version will resolve either `Finchley.x` branch or will fallback to `master` if there's no `Finchley.x` branch. +** for `spring-cloud-sleuth`'s `2.1.0.RELEASE` version will resolve `2.1.x` branch +- Performs the release tasks per each project +- Performs the post release tasks at the end of the release + +IMPORTANT: For the meta-releaser to work we assume that the path to the +custom configuration file for each project is always `config/releaser.yml`. === What should I do first? @@ -215,10 +234,31 @@ $ java -jar ~/repo/spring-cloud-release-tools/spring-cloud-release-tools-spring/ $ 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 ---- +=== How to run meta-release (automatic-mode) + +All you have to do is run the jar with the releaser and pass the +`-x=true` option to turn on meta-release and a list of fixed versions +in the `--"releaser.fixed-versions[project-name]=project-version" format + +``` +$ java -jar spring-cloud-release-tools-spring/target/spring-cloud-release-tools-spring-1.0.0.BUILD-SNAPSHOT.jar --spring.config.name=releaser -x=true --"releaser.fixed-versions[spring-cloud-sleuth]=2.0.1.BUILD-SNAPSHOT" +``` + +IMPORTANT: For the meta release the `startFrom` or `taskNames` take into consideration +the project names, not task names. E.g. you can start from `spring-cloud-netflix` project, +or build only tasks with names `spring-cloud-build,spring-cloud-sleuth`. + === 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 the `spring-cloud-cli.version` to `1.0.0.RELEASE` regardless of what was set in `spring-cloud-release` project. Example `--releaser.fixed-versions[spring-cloud-cli]=1.0.0.RELEASE`. +Use these properties to provide versions for the meta release. + +- `releaser.meta-release.enabled` - You have to turn it on to enable a meta release. Defaults to `false` +- `releaser.meta-release.release-train-project-name` - Name of the project that represents the BOM of the release train. Defaults to `spring-cloud-release` +- `releaser.meta-release.git-org-url` - The URL of the Git organization. We'll append each project's name to it. +Defaults to `https://github.com/spring-cloud` + - `releaser.git.fetch-versions-from-git` - If `true` then should fill the map of versions from Git. If `false` then picks fixed versions - `releaser.git.clone-destination-dir` - Where should the Spring Cloud Release repo get cloned to. If null defaults to a temporary directory - `releaser.git.spring-cloud-release-git-url` - URL to Spring Cloud Release Git repository. Defaults to `https://github.com/spring-cloud/spring-cloud-release` @@ -272,6 +312,9 @@ $ java -jar target/spring-cloud-release-tools-spring-1.0.0.M1.jar --spring.confi TIP: Notice that we're downloading the jar to a parent folder, not to `target`. That's because `target` get cleaned during the build process +IMPORTANT: For the meta-releaser to work we assume that the path to the +configuration file is always `config/releaser.yml`. + ==== Specifying A Branch By deafult the releaser will default to using the `master` branch of `spring-cloud-release`. diff --git a/pom.xml b/pom.xml index e846bede..51b1df5c 100644 --- a/pom.xml +++ b/pom.xml @@ -12,7 +12,7 @@ org.springframework.cloud spring-cloud-build - 1.3.9.RELEASE + 2.0.0.RELEASE @@ -24,7 +24,7 @@ - Edgware.BUILD-SNAPSHOT + Finchley.RELEASE diff --git a/spring-cloud-release-tools-core/foo b/spring-cloud-release-tools-core/foo new file mode 100644 index 00000000..e69de29b diff --git a/spring-cloud-release-tools-core/pom.xml b/spring-cloud-release-tools-core/pom.xml index 1543863c..2b1b3783 100644 --- a/spring-cloud-release-tools-core/pom.xml +++ b/spring-cloud-release-tools-core/pom.xml @@ -37,6 +37,7 @@ org.hibernate hibernate-validator + 5.4.1.Final org.eclipse.jgit diff --git a/spring-cloud-release-tools-core/src/main/java/org/springframework/cloud/release/internal/Releaser.java b/spring-cloud-release-tools-core/src/main/java/org/springframework/cloud/release/internal/Releaser.java index 78308787..5f45afe9 100644 --- a/spring-cloud-release-tools-core/src/main/java/org/springframework/cloud/release/internal/Releaser.java +++ b/spring-cloud-release-tools-core/src/main/java/org/springframework/cloud/release/internal/Releaser.java @@ -46,6 +46,10 @@ public class Releaser { this.documentationUpdater = documentationUpdater; } + public File clonedProjectFromOrg(String projectName) { + return this.projectGitHandler.cloneProjectFromOrg(projectName); + } + public Projects retrieveVersionsFromSCRelease() { return this.projectPomUpdater.retrieveVersionsFromSCRelease(); } diff --git a/spring-cloud-release-tools-core/src/main/java/org/springframework/cloud/release/internal/ReleaserProperties.java b/spring-cloud-release-tools-core/src/main/java/org/springframework/cloud/release/internal/ReleaserProperties.java index c64f8fc1..0b6be61a 100644 --- a/spring-cloud-release-tools-core/src/main/java/org/springframework/cloud/release/internal/ReleaserProperties.java +++ b/spring-cloud-release-tools-core/src/main/java/org/springframework/cloud/release/internal/ReleaserProperties.java @@ -45,8 +45,56 @@ public class ReleaserProperties { private Sagan sagan = new Sagan(); + /** + * Project name to its version - overrides all versions + * retrieved from a repository like Spring Cloud Release + */ private Map fixedVersions = new HashMap<>(); + private MetaRelease metaRelease = new MetaRelease(); + + public static class MetaRelease { + /** + * Are we releasing the whole suite of apps or only one? + */ + private boolean enabled = false; + + /** + * Name of the release train project + */ + private String releaseTrainProjectName = "spring-cloud-release"; + + /** + * The URL of the Git organization. We'll append each project's + * name to it + */ + private String gitOrgUrl = "https://github.com/spring-cloud"; + + public boolean isEnabled() { + return this.enabled; + } + + public void setEnabled(boolean enabled) { + this.enabled = enabled; + } + + public String getGitOrgUrl() { + return this.gitOrgUrl; + } + + public void setGitOrgUrl(String gitOrgUrl) { + this.gitOrgUrl = gitOrgUrl; + } + + public String getReleaseTrainProjectName() { + return this.releaseTrainProjectName; + } + + public void setReleaseTrainProjectName(String releaseTrainProjectName) { + this.releaseTrainProjectName = releaseTrainProjectName; + } + } + public static class Git { /** @@ -388,6 +436,14 @@ public class ReleaserProperties { this.fixedVersions = fixedVersions; } + public MetaRelease getMetaRelease() { + return this.metaRelease; + } + + public void setMetaRelease(MetaRelease metaRelease) { + this.metaRelease = metaRelease; + } + public Sagan getSagan() { return this.sagan; } diff --git a/spring-cloud-release-tools-core/src/main/java/org/springframework/cloud/release/internal/ReleaserPropertiesAware.java b/spring-cloud-release-tools-core/src/main/java/org/springframework/cloud/release/internal/ReleaserPropertiesAware.java new file mode 100644 index 00000000..99b0c251 --- /dev/null +++ b/spring-cloud-release-tools-core/src/main/java/org/springframework/cloud/release/internal/ReleaserPropertiesAware.java @@ -0,0 +1,9 @@ +package org.springframework.cloud.release.internal; + +/** + * @author Marcin Grzejszczak + */ +public interface ReleaserPropertiesAware { + + void setReleaserProperties(ReleaserProperties properties); +} diff --git a/spring-cloud-release-tools-core/src/main/java/org/springframework/cloud/release/internal/git/GitRepo.java b/spring-cloud-release-tools-core/src/main/java/org/springframework/cloud/release/internal/git/GitRepo.java index ad78cff3..e6c4bc98 100644 --- a/spring-cloud-release-tools-core/src/main/java/org/springframework/cloud/release/internal/git/GitRepo.java +++ b/spring-cloud-release-tools-core/src/main/java/org/springframework/cloud/release/internal/git/GitRepo.java @@ -112,13 +112,12 @@ class GitRepo { /** * Checks out a branch for a project - * @param project - a Git project * @param branch - branch to check out */ - void checkout(File project, String branch) { + void checkout(String branch) { try { log.info("Checking out branch [{}] for repo [{}]", branch, this.basedir); - checkoutBranch(project, branch); + checkoutBranch(this.basedir, branch); log.info("Successfully checked out the branch [{}]", branch); } catch (Exception e) { @@ -128,11 +127,10 @@ class GitRepo { /** * Performs a commit - * @param project - a Git project * @param message - commit message */ - void commit(File project, String message) { - try(Git git = this.gitFactory.open(file(project))) { + void commit(String message) { + try(Git git = this.gitFactory.open(file(this.basedir))) { git.add().addFilepattern(".").call(); git.commit().setAllowEmpty(false).setMessage(message).call(); printLog(git); @@ -143,6 +141,16 @@ class GitRepo { } } + boolean hasBranch(String branch) { + try(Git git = this.gitFactory.open(file(this.basedir))) { + List refs = git.branchList().setListMode(ListBranchCommand.ListMode.ALL) + .call(); + return refs.stream().anyMatch(ref -> ref.getName().contains(branch)); + } catch (Exception e) { + throw new IllegalStateException(e); + } + } + private void printLog(Git git) throws GitAPIException, IOException { int maxCount = 5; String currentBranch = git.getRepository().getBranch(); @@ -155,11 +163,10 @@ class GitRepo { /** * Creates a tag with a given name - * @param project * @param tagName */ - void tag(File project, String tagName) { - try(Git git = this.gitFactory.open(file(project))) { + void tag(String tagName) { + try(Git git = this.gitFactory.open(file(this.basedir))) { git.tag().setName(tagName).call(); } catch (Exception e) { throw new IllegalStateException(e); @@ -168,11 +175,10 @@ class GitRepo { /** * Pushes the commits to {@code origin} remote branch - * @param project - Git project * @param branch - remote branch to which the code should be pushed */ - void pushBranch(File project, String branch) { - try(Git git = this.gitFactory.open(file(project))) { + void pushBranch(String branch) { + try(Git git = this.gitFactory.open(file(this.basedir))) { String localBranch = git.getRepository().getFullBranch(); RefSpec refSpec = new RefSpec(localBranch + ":" + branch); this.gitFactory.push(git).setPushTags().setRefSpecs(refSpec).call(); @@ -183,10 +189,9 @@ class GitRepo { /** * Pushes the commits od current branch - * @param project - Git project */ - void pushCurrentBranch(File project) { - try(Git git = this.gitFactory.open(file(project))) { + void pushCurrentBranch() { + try(Git git = this.gitFactory.open(file(this.basedir))) { this.gitFactory.push(git).call(); } catch (Exception e) { throw new IllegalStateException(e); @@ -195,11 +200,10 @@ class GitRepo { /** * Pushes the commits to {@code origin} remote tag - * @param project - Git project * @param tagName - remote tag to which the code should be pushed */ - void pushTag(File project, String tagName) { - try(Git git = this.gitFactory.open(file(project))) { + void pushTag(String tagName) { + try(Git git = this.gitFactory.open(file(this.basedir))) { String localBranch = git.getRepository().getFullBranch(); RefSpec refSpec = new RefSpec(localBranch + ":" + "refs/tags/" + tagName); this.gitFactory.push(git).setPushTags().setRefSpecs(refSpec).call(); @@ -208,8 +212,8 @@ class GitRepo { } } - void revert(File project, String message) { - try(Git git = this.gitFactory.open(file(project))) { + void revert(String message) { + try(Git git = this.gitFactory.open(file(this.basedir))) { RevCommit commit = git.log().setMaxCount(1).call().iterator().next(); String shortMessage = commit.getShortMessage(); String id = commit.getId().getName(); @@ -227,8 +231,8 @@ class GitRepo { } } - String currentBranch(File project) { - try(Git git = this.gitFactory.open(file(project))) { + String currentBranch() { + try(Git git = this.gitFactory.open(file(this.basedir))) { return git.getRepository().getBranch(); } catch (Exception e) { throw new IllegalStateException(e); diff --git a/spring-cloud-release-tools-core/src/main/java/org/springframework/cloud/release/internal/git/ProjectGitHandler.java b/spring-cloud-release-tools-core/src/main/java/org/springframework/cloud/release/internal/git/ProjectGitHandler.java index b62f28ff..3f31810e 100644 --- a/spring-cloud-release-tools-core/src/main/java/org/springframework/cloud/release/internal/git/ProjectGitHandler.java +++ b/spring-cloud-release-tools-core/src/main/java/org/springframework/cloud/release/internal/git/ProjectGitHandler.java @@ -8,15 +8,17 @@ import java.nio.file.Files; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.cloud.release.internal.ReleaserProperties; +import org.springframework.cloud.release.internal.ReleaserPropertiesAware; import org.springframework.cloud.release.internal.pom.ProjectVersion; import org.springframework.cloud.release.internal.pom.Projects; +import org.springframework.util.StringUtils; /** * Contains business logic around Git & Github operations * * @author Marcin Grzejszczak */ -public class ProjectGitHandler { +public class ProjectGitHandler implements ReleaserPropertiesAware { private static final Logger log = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass()); @@ -25,7 +27,7 @@ public class ProjectGitHandler { private static final String POST_RELEASE_MSG = "Going back to snapshots"; private static final String POST_RELEASE_BUMP_MSG = "Bumping versions to %s after release"; - private final ReleaserProperties properties; + private ReleaserProperties properties; private final GithubMilestones githubMilestones; private final GithubIssues githubIssues; @@ -39,13 +41,13 @@ public class ProjectGitHandler { GitRepo gitRepo = gitRepo(project); if (version.isSnapshot()) { log.info("Snapshot version [{}] found. Will only commit the changed poms", version); - gitRepo.commit(project, MSG); + gitRepo.commit(MSG); } else { log.info("NON-snapshot version [{}] found. Will commit the changed poms, tag the version and push the tag", version); - gitRepo.commit(project, String.format(PRE_RELEASE_MSG, version.version)); + gitRepo.commit(String.format(PRE_RELEASE_MSG, version.version)); String tagName = "v" + version.version; - gitRepo.tag(project, tagName); - gitRepo.pushTag(project, tagName); + gitRepo.tag(tagName); + gitRepo.pushTag(tagName); } } @@ -60,7 +62,7 @@ public class ProjectGitHandler { public void commit(File project, String message) { GitRepo gitRepo = gitRepo(project); - gitRepo.commit(project, message); + gitRepo.commit(message); } public File cloneScReleaseProject() { @@ -73,7 +75,36 @@ public class ProjectGitHandler { return clonedProject; } - private File cloneProject(String url) { + /** + * For meta-release. Works with fixed versions only + * @param projectName - name of the project to clone + * @return location of the cloned project + */ + public File cloneProjectFromOrg(String projectName) { + String orgUrl = this.properties.getMetaRelease().getGitOrgUrl(); + String fullUrl = orgUrl.endsWith("/") ? orgUrl + projectName : orgUrl + "/" + + projectName + suffixNonHttpRepo(orgUrl); + File clonedProject = cloneProject(fullUrl); + String version = this.properties.getFixedVersions().get(projectName); + if (StringUtils.isEmpty(version)) { + throw new IllegalStateException("You haven't provided a version for project [" + projectName + "]"); + } + String branchFromVersion = branchFromVersion(version); + boolean branchExists = gitRepo(clonedProject).hasBranch(branchFromVersion); + if (!branchExists) { + log.info("Branch [{}] does not exist. Assuming that should work with master branch", branchFromVersion); + return clonedProject; + } + log.info("Branch [{}] exists. Will check it out", branchFromVersion); + checkout(clonedProject, branchFromVersion); + return clonedProject; + } + + private String suffixNonHttpRepo(String orgUrl) { + return orgUrl.startsWith("http") || orgUrl.startsWith("git") ? "" : "/"; + } + + File cloneProject(String url) { try { File destinationDir = properties.getGit().getCloneDestinationDir() != null ? new File(properties.getGit().getCloneDestinationDir()) : @@ -85,7 +116,28 @@ public class ProjectGitHandler { } public void checkout(File project, String branch) { - gitRepo(project).checkout(project, branch); + gitRepo(project).checkout(branch); + } + + // let's go with convention... If fixed version contains e.g. + // 2.3.4.RELEASE of Sleuth, we will first check if `2.0.x` branch + // exists. If not, then we will assume that `master` contains it + private String branchFromVersion(String version) { + // 2.3.4.RELEASE -> 2.3.4 + // Camden.RELEASE -> Camden + String versionTillPatch = version.substring(0, version.lastIndexOf(".")); + // 2.3.4 -> [2,3,4] + // Camden -> [Camden] + String[] splitVersion = versionTillPatch.split("\\."); + if (splitVersion.length == 3) { + // [2,3,4] -> 2.3.x + return splitVersion[0] + "." + splitVersion[1] + ".x"; + } else if (splitVersion.length == 1) { + // [Camden] -> [Camden.x] + return splitVersion[0] + ".x"; + } + throw new IllegalStateException("Wrong version [" + version + "]. Can't extract semver pieces of it"); + } public void revertChangesIfApplicable(File project, ProjectVersion version) { @@ -94,11 +146,11 @@ public class ProjectGitHandler { return; } log.info("Reverting last commit"); - gitRepo(project).revert(project, POST_RELEASE_MSG); + gitRepo(project).revert(POST_RELEASE_MSG); } public void pushCurrentBranch(File project) { - gitRepo(project).pushCurrentBranch(project); + gitRepo(project).pushCurrentBranch(); } public void closeMilestone(ProjectVersion releaseVersion) { @@ -114,10 +166,14 @@ public class ProjectGitHandler { } public String currentBranch(File project) { - return gitRepo(project).currentBranch(project); + return gitRepo(project).currentBranch(); } GitRepo gitRepo(File workingDir) { return new GitRepo(workingDir, this.properties); } + + @Override public void setReleaserProperties(ReleaserProperties properties) { + this.properties = properties; + } } diff --git a/spring-cloud-release-tools-core/src/main/java/org/springframework/cloud/release/internal/gradle/GradleUpdater.java b/spring-cloud-release-tools-core/src/main/java/org/springframework/cloud/release/internal/gradle/GradleUpdater.java index c8a820c6..14c664ca 100644 --- a/spring-cloud-release-tools-core/src/main/java/org/springframework/cloud/release/internal/gradle/GradleUpdater.java +++ b/spring-cloud-release-tools-core/src/main/java/org/springframework/cloud/release/internal/gradle/GradleUpdater.java @@ -17,17 +17,18 @@ import java.util.concurrent.atomic.AtomicReference; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.cloud.release.internal.ReleaserProperties; +import org.springframework.cloud.release.internal.ReleaserPropertiesAware; import org.springframework.cloud.release.internal.pom.ProjectVersion; import org.springframework.cloud.release.internal.pom.Projects; /** * @author Marcin Grzejszczak */ -public class GradleUpdater { +public class GradleUpdater implements ReleaserPropertiesAware { private static final Logger log = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass()); - private final ReleaserProperties properties; + private ReleaserProperties properties; public GradleUpdater(ReleaserProperties properties) { this.properties = properties; @@ -59,6 +60,10 @@ public class GradleUpdater { } } + @Override public void setReleaserProperties(ReleaserProperties properties) { + this.properties = properties; + } + private class GradlePropertiesWalker extends SimpleFileVisitor { private static final String GRADLE_PROPERTIES = "gradle.properties"; diff --git a/spring-cloud-release-tools-core/src/main/java/org/springframework/cloud/release/internal/pom/ProjectPomUpdater.java b/spring-cloud-release-tools-core/src/main/java/org/springframework/cloud/release/internal/pom/ProjectPomUpdater.java index 4f2451f5..a4cbf82b 100644 --- a/spring-cloud-release-tools-core/src/main/java/org/springframework/cloud/release/internal/pom/ProjectPomUpdater.java +++ b/spring-cloud-release-tools-core/src/main/java/org/springframework/cloud/release/internal/pom/ProjectPomUpdater.java @@ -29,12 +29,13 @@ import java.util.Scanner; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.cloud.release.internal.ReleaserProperties; +import org.springframework.cloud.release.internal.ReleaserPropertiesAware; import org.springframework.cloud.release.internal.git.ProjectGitHandler; /** * @author Marcin Grzejszczak */ -public class ProjectPomUpdater { +public class ProjectPomUpdater implements ReleaserPropertiesAware { private static final List IGNORED_SNAPSHOT_LINE_PATTERNS = Arrays.asList( "^.*replace=.*$", @@ -44,7 +45,7 @@ public class ProjectPomUpdater { private static final Logger log = LoggerFactory.getLogger(ProjectPomUpdater.class); - private final ReleaserProperties properties; + private ReleaserProperties properties; private final ProjectGitHandler gitRepo; private final PomUpdater pomUpdater = new PomUpdater(); @@ -103,6 +104,10 @@ public class ProjectPomUpdater { } } + @Override public void setReleaserProperties(ReleaserProperties properties) { + this.properties = properties; + } + private class PomWalker extends SimpleFileVisitor { private static final String POM_XML = "pom.xml"; diff --git a/spring-cloud-release-tools-core/src/main/java/org/springframework/cloud/release/internal/pom/Projects.java b/spring-cloud-release-tools-core/src/main/java/org/springframework/cloud/release/internal/pom/Projects.java index 6b51710d..75fd8b75 100644 --- a/spring-cloud-release-tools-core/src/main/java/org/springframework/cloud/release/internal/pom/Projects.java +++ b/spring-cloud-release-tools-core/src/main/java/org/springframework/cloud/release/internal/pom/Projects.java @@ -66,6 +66,11 @@ public class Projects extends HashSet { .orElseThrow(() -> exception(projectName)); } + public boolean containsProject(String projectName) { + return this.stream() + .anyMatch(projectVersion -> projectVersion.projectName.equals(projectName)); + } + public List forNameStartingWith(String projectName) { return this.stream().filter(projectVersion -> projectVersion.projectName.startsWith(projectName)) .collect(Collectors.toList()); diff --git a/spring-cloud-release-tools-core/src/main/java/org/springframework/cloud/release/internal/pom/SCReleasePomParser.java b/spring-cloud-release-tools-core/src/main/java/org/springframework/cloud/release/internal/pom/SCReleasePomParser.java index f54cf491..144445ec 100644 --- a/spring-cloud-release-tools-core/src/main/java/org/springframework/cloud/release/internal/pom/SCReleasePomParser.java +++ b/spring-cloud-release-tools-core/src/main/java/org/springframework/cloud/release/internal/pom/SCReleasePomParser.java @@ -45,7 +45,7 @@ class SCReleasePomParser { private final File springCloudReleaseDir; private final String bootPom; - private final String dependenciesPom; + private final String dependenciesPomPath; private final PomReader pomReader = new PomReader(); SCReleasePomParser(File springCloudReleaseDir) { @@ -55,7 +55,7 @@ class SCReleasePomParser { SCReleasePomParser(File springCloudReleaseDir, String bootPom, String dependenciesPom) { this.springCloudReleaseDir = springCloudReleaseDir; this.bootPom = bootPom; - this.dependenciesPom = dependenciesPom; + this.dependenciesPomPath = dependenciesPom; } Versions allVersions() { @@ -96,7 +96,7 @@ class SCReleasePomParser { } Versions springCloudVersions() { - Model model = pom(this.dependenciesPom); + Model model = pom(this.dependenciesPomPath); String buildArtifact = model.getParent().getArtifactId(); log.debug("[{}] artifact id is equal to [{}]", SpringCloudConstants.CLOUD_DEPENDENCIES_ARTIFACT_ID, buildArtifact); if (!SpringCloudConstants.CLOUD_DEPENDENCIES_ARTIFACT_ID.equals(buildArtifact)) { @@ -109,6 +109,8 @@ class SCReleasePomParser { .filter(propertyMatchesSCPattern()) .map(toProject()) .collect(Collectors.toSet()); + String scReleaseVersion = model.getVersion(); + projects.add(new Project("spring-cloud-release", scReleaseVersion)); return new Versions(buildVersion, projects); } diff --git a/spring-cloud-release-tools-core/src/main/java/org/springframework/cloud/release/internal/project/ProjectBuilder.java b/spring-cloud-release-tools-core/src/main/java/org/springframework/cloud/release/internal/project/ProjectBuilder.java index f13e4abd..ff909ae8 100644 --- a/spring-cloud-release-tools-core/src/main/java/org/springframework/cloud/release/internal/project/ProjectBuilder.java +++ b/spring-cloud-release-tools-core/src/main/java/org/springframework/cloud/release/internal/project/ProjectBuilder.java @@ -18,6 +18,7 @@ import java.util.stream.Collectors; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import org.springframework.cloud.release.internal.ReleaserPropertiesAware; import org.springframework.cloud.release.internal.ReleaserProperties; import org.springframework.cloud.release.internal.pom.ProjectVersion; import org.springframework.util.StringUtils; @@ -25,12 +26,12 @@ import org.springframework.util.StringUtils; /** * @author Marcin Grzejszczak */ -public class ProjectBuilder { +public class ProjectBuilder implements ReleaserPropertiesAware { private static final Logger log = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass()); private static final String VERSION_MUSTACHE = "{{version}}"; - private final ReleaserProperties properties; + private ReleaserProperties properties; private final ProcessExecutor executor; public ProjectBuilder(ReleaserProperties properties) { @@ -161,12 +162,17 @@ public class ProjectBuilder { } return commandsList.toArray(new String[commandsList.size()]); } + + @Override public void setReleaserProperties(ReleaserProperties properties) { + this.properties = properties; + this.executor.setReleaserProperties(properties); + } } -class ProcessExecutor { +class ProcessExecutor implements ReleaserPropertiesAware { private static final Logger log = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass()); - private final ReleaserProperties properties; + private ReleaserProperties properties; ProcessExecutor(ReleaserProperties properties) { this.properties = properties; @@ -175,7 +181,8 @@ class ProcessExecutor { void runCommand(String[] commands, long waitTimeInMinutes) { try { String workingDir = this.properties.getWorkingDir(); - log.debug("Will run the build via {} and wait for result for [{}] minutes", commands, waitTimeInMinutes); + log.debug("Will run the build from [{}] via {} and wait for result for [{}] minutes", + workingDir, commands, waitTimeInMinutes); ProcessBuilder builder = builder(commands, workingDir); Process process = startProcess(builder); boolean finished = process.waitFor(waitTimeInMinutes, TimeUnit.MINUTES); @@ -202,6 +209,10 @@ class ProcessExecutor { .directory(new File(workingDir)) .inheritIO(); } + + @Override public void setReleaserProperties(ReleaserProperties properties) { + this.properties = properties; + } } class HtmlFileWalker extends SimpleFileVisitor { diff --git a/spring-cloud-release-tools-core/src/main/java/org/springframework/cloud/release/internal/template/TemplateGenerator.java b/spring-cloud-release-tools-core/src/main/java/org/springframework/cloud/release/internal/template/TemplateGenerator.java index 9475f151..878d7e54 100644 --- a/spring-cloud-release-tools-core/src/main/java/org/springframework/cloud/release/internal/template/TemplateGenerator.java +++ b/spring-cloud-release-tools-core/src/main/java/org/springframework/cloud/release/internal/template/TemplateGenerator.java @@ -9,13 +9,14 @@ import java.io.File; import java.io.IOException; import org.springframework.cloud.release.internal.ReleaserProperties; +import org.springframework.cloud.release.internal.ReleaserPropertiesAware; import org.springframework.cloud.release.internal.git.ProjectGitHandler; import org.springframework.cloud.release.internal.pom.Projects; /** * @author Marcin Grzejszczak */ -public class TemplateGenerator { +public class TemplateGenerator implements ReleaserPropertiesAware { private static final String EMAIL_TEMPLATE = "email"; private static final String BLOG_TEMPLATE = "blog"; @@ -25,7 +26,7 @@ public class TemplateGenerator { private final File blogOutput; private final File tweetOutput; private final File releaseNotesOutput; - private final ReleaserProperties props; + private ReleaserProperties props; private final ProjectGitHandler handler; public TemplateGenerator(ReleaserProperties props, ProjectGitHandler handler) { @@ -109,4 +110,8 @@ public class TemplateGenerator { throw new IllegalStateException(e); } } + + @Override public void setReleaserProperties(ReleaserProperties properties) { + this.props = properties; + } } diff --git a/spring-cloud-release-tools-core/src/test/java/org/springframework/cloud/release/internal/git/GitRepoTests.java b/spring-cloud-release-tools-core/src/test/java/org/springframework/cloud/release/internal/git/GitRepoTests.java index 0a082da0..52998b83 100644 --- a/spring-cloud-release-tools-core/src/test/java/org/springframework/cloud/release/internal/git/GitRepoTests.java +++ b/spring-cloud-release-tools-core/src/test/java/org/springframework/cloud/release/internal/git/GitRepoTests.java @@ -70,7 +70,7 @@ public class GitRepoTests { @Test public void should_check_out_a_branch_on_cloned_repo() throws IOException { File project = this.gitRepo.cloneProject(this.springCloudReleaseProject.toURI()); - this.gitRepo.checkout(project, "vCamden.SR3"); + new GitRepo(project).checkout("vCamden.SR3"); File pom = new File(this.tmpFolder, "pom.xml"); then(pom).exists(); @@ -80,18 +80,32 @@ public class GitRepoTests { @Test public void should_check_out_a_branch_on_cloned_repo2() throws IOException { File project = this.gitRepo.cloneProject(this.springCloudReleaseProject.toURI()); - this.gitRepo.checkout(project, "Camden.x"); + new GitRepo(project).checkout("Camden.x"); File pom = new File(this.tmpFolder, "pom.xml"); then(pom).exists(); then(Files.lines(pom.toPath()).anyMatch(s -> s.contains("Camden.BUILD-SNAPSHOT"))).isTrue(); } + @Test + public void should_return_true_if_branch_exists() throws IOException { + File project = new GitRepo(this.tmpFolder).cloneProject(this.springCloudReleaseProject.toURI()); + + then(new GitRepo(project).hasBranch("Camden.x")).isTrue(); + } + + @Test + public void should_return_false_if_branch_does_not_exist() throws IOException { + File project = new GitRepo(this.tmpFolder).cloneProject(this.springCloudReleaseProject.toURI()); + + then(new GitRepo(project).hasBranch("aksjdhkasjkajshd")).isFalse(); + } + @Test public void should_throw_an_exception_when_checking_out_nonexisting_branch() throws IOException { - File project = this.gitRepo.cloneProject(this.springCloudReleaseProject.toURI()); + File project = new GitRepo(this.tmpFolder).cloneProject(this.springCloudReleaseProject.toURI()); try { - this.gitRepo.checkout(project, "nonExistingBranch"); + new GitRepo(project).checkout("nonExistingBranch"); fail("should throw an exception"); } catch (IllegalStateException e) { then(e).hasMessageContaining("Ref nonExistingBranch can not be resolved"); @@ -100,10 +114,10 @@ public class GitRepoTests { @Test public void should_commit_changes() throws Exception { - File project = this.gitRepo.cloneProject(this.springCloudReleaseProject.toURI()); + File project = new GitRepo(this.tmpFolder).cloneProject(this.springCloudReleaseProject.toURI()); createNewFile(project); - this.gitRepo.commit(project, "some message"); + new GitRepo(project).commit("some message"); try(Git git = openGitProject(project)) { RevCommit revCommit = git.log().call().iterator().next(); @@ -113,11 +127,11 @@ public class GitRepoTests { @Test public void should_not_commit_empty_changes() throws Exception { - File project = this.gitRepo.cloneProject(this.springCloudReleaseProject.toURI()); + File project = new GitRepo(this.tmpFolder).cloneProject(this.springCloudReleaseProject.toURI()); createNewFile(project); - this.gitRepo.commit(project, "some message"); + new GitRepo(project).commit("some message"); - this.gitRepo.commit(project, "empty commit"); + new GitRepo(project).commit("empty commit"); try(Git git = openGitProject(project)) { RevCommit revCommit = git.log().call().iterator().next(); @@ -127,11 +141,11 @@ public class GitRepoTests { @Test public void should_create_a_tag() throws Exception { - File project = this.gitRepo.cloneProject(this.springCloudReleaseProject.toURI()); + File project = new GitRepo(this.tmpFolder).cloneProject(this.springCloudReleaseProject.toURI()); createNewFile(project); - this.gitRepo.commit(project, "some message"); + new GitRepo(project).commit("some message"); - this.gitRepo.tag(project, "v1.0.0"); + new GitRepo(project).tag("v1.0.0"); try(Git git = openGitProject(project)) { tagIsPresent(git, "v1.0.0"); @@ -147,12 +161,12 @@ public class GitRepoTests { @Test public void should_push_changes_to_master_branch() throws Exception { File origin = clonedProject(this.tmp.newFolder(), this.springCloudReleaseProject); - File project = this.gitRepo.cloneProject(this.springCloudReleaseProject.toURI()); + File project = new GitRepo(this.tmpFolder).cloneProject(this.springCloudReleaseProject.toURI()); setOriginOnProjectToTmp(origin, project); createNewFile(project); - this.gitRepo.commit(project, "some message"); + new GitRepo(project).commit("some message"); - this.gitRepo.pushBranch(project, "master"); + new GitRepo(project).pushBranch("master"); try(Git git = openGitProject(origin)) { RevCommit revCommit = git.log().call().iterator().next(); @@ -163,12 +177,12 @@ public class GitRepoTests { @Test public void should_push_changes_to_current_branch() throws Exception { File origin = clonedProject(this.tmp.newFolder(), this.springCloudReleaseProject); - File project = this.gitRepo.cloneProject(this.springCloudReleaseProject.toURI()); + File project = new GitRepo(this.tmpFolder).cloneProject(this.springCloudReleaseProject.toURI()); setOriginOnProjectToTmp(origin, project); createNewFile(project); - this.gitRepo.commit(project, "some message"); + new GitRepo(project).commit("some message"); - this.gitRepo.pushCurrentBranch(project); + new GitRepo(project).pushCurrentBranch(); try(Git git = openGitProject(origin)) { RevCommit revCommit = git.log().call().iterator().next(); @@ -179,11 +193,11 @@ public class GitRepoTests { @Test public void should_return_the_branch_name() throws Exception { File origin = clonedProject(this.tmp.newFolder(), this.springCloudReleaseProject); - File project = this.gitRepo.cloneProject(this.springCloudReleaseProject.toURI()); + File project = new GitRepo(this.tmpFolder).cloneProject(this.springCloudReleaseProject.toURI()); setOriginOnProjectToTmp(origin, project); createNewFile(project); - String branch = this.gitRepo.currentBranch(project); + String branch = new GitRepo(project).currentBranch(); then(branch).isEqualTo("master"); } @@ -191,13 +205,13 @@ public class GitRepoTests { @Test public void should_push_a_tag_to_new_branch_in_origin() throws Exception { File origin = clonedProject(this.tmp.newFolder(), this.springCloudReleaseProject); - File project = this.gitRepo.cloneProject(this.springCloudReleaseProject.toURI()); + File project = new GitRepo(this.tmpFolder).cloneProject(this.springCloudReleaseProject.toURI()); setOriginOnProjectToTmp(origin, project); createNewFile(project); - this.gitRepo.commit(project, "some message"); - this.gitRepo.tag(project, "v5.6.7.RELEASE"); + new GitRepo(project).commit("some message"); + new GitRepo(project).tag("v5.6.7.RELEASE"); - this.gitRepo.pushTag(project, "v5.6.7.RELEASE"); + new GitRepo(project).pushTag("v5.6.7.RELEASE"); try(Git git = openGitProject(origin)) { tagIsPresent(git, "v5.6.7"); @@ -225,12 +239,12 @@ public class GitRepoTests { @Test public void should_revert_changes() throws Exception { - File project = this.gitRepo.cloneProject(this.springCloudReleaseProject.toURI()); + File project = new GitRepo(this.tmpFolder).cloneProject(this.springCloudReleaseProject.toURI()); File foo = new File(project, "foo"); foo.createNewFile(); - this.gitRepo.commit(project, "Update SNAPSHOT to 1.0.0.RC1"); + new GitRepo(project).commit("Update SNAPSHOT to 1.0.0.RC1"); - this.gitRepo.revert(project, "Reverting the commit"); + new GitRepo(project).revert("Reverting the commit"); try(Git git = openGitProject(project)) { RevCommit revCommit = git.log().call().iterator().next(); @@ -240,10 +254,10 @@ public class GitRepoTests { @Test public void should_not_revert_changes_when_commit_message_is_not_related_to_updating_snapshots() throws Exception { - File project = this.gitRepo.cloneProject(this.springCloudReleaseProject.toURI()); + File project = new GitRepo(tmpFolder).cloneProject(this.springCloudReleaseProject.toURI()); BDDAssertions.thenThrownBy( - () -> this.gitRepo.revert(project, "some message")) + () -> new GitRepo(project).revert("some message")) .hasMessageContaining("Won't revert the commit with id"); } diff --git a/spring-cloud-release-tools-core/src/test/java/org/springframework/cloud/release/internal/git/ProjectGitHandlerTests.java b/spring-cloud-release-tools-core/src/test/java/org/springframework/cloud/release/internal/git/ProjectGitHandlerTests.java index 144d7854..8b289db1 100644 --- a/spring-cloud-release-tools-core/src/test/java/org/springframework/cloud/release/internal/git/ProjectGitHandlerTests.java +++ b/spring-cloud-release-tools-core/src/test/java/org/springframework/cloud/release/internal/git/ProjectGitHandlerTests.java @@ -2,13 +2,16 @@ package org.springframework.cloud.release.internal.git; import java.io.File; +import org.assertj.core.api.BDDAssertions; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.Mock; import org.mockito.runners.MockitoJUnitRunner; import org.springframework.cloud.release.internal.ReleaserProperties; import org.springframework.cloud.release.internal.pom.ProjectVersion; +import org.springframework.cloud.release.internal.sagan.Release; +import static org.mockito.BDDMockito.given; import static org.mockito.BDDMockito.then; import static org.mockito.Matchers.any; import static org.mockito.Matchers.anyString; @@ -22,10 +25,15 @@ import static org.mockito.Mockito.never; public class ProjectGitHandlerTests { @Mock GitRepo gitRepo; - ProjectGitHandler updater = new ProjectGitHandler(new ReleaserProperties()) { + ReleaserProperties properties = new ReleaserProperties(); + ProjectGitHandler updater = new ProjectGitHandler(this.properties) { @Override GitRepo gitRepo(File workingDir) { return ProjectGitHandlerTests.this.gitRepo; } + + @Override File cloneProject(String url) { + return new File("."); + } }; File file = new File(""); @@ -33,54 +41,95 @@ public class ProjectGitHandlerTests { public void should_only_commit_without_pushing_changes_when_version_is_snapshot() { this.updater.commitAndTagIfApplicable(this.file, projectVersion("1.0.0.BUILD-SNAPSHOT")); - then(this.gitRepo).should().commit(any(File.class), eq("Bumping versions")); - then(this.gitRepo).should(never()).tag(any(File.class), anyString()); + then(this.gitRepo).should().commit(eq("Bumping versions")); + then(this.gitRepo).should(never()).tag(anyString()); } @Test public void should_commit_tag_and_push_tag_when_version_is_not_snapshot() { this.updater.commitAndTagIfApplicable(this.file, projectVersion("1.0.0.RELEASE")); - then(this.gitRepo).should().commit(any(File.class), eq("Update SNAPSHOT to 1.0.0.RELEASE")); - then(this.gitRepo).should().tag(any(File.class), eq("v1.0.0.RELEASE")); - then(this.gitRepo).should().pushTag(any(File.class), eq("v1.0.0.RELEASE")); + then(this.gitRepo).should().commit(eq("Update SNAPSHOT to 1.0.0.RELEASE")); + then(this.gitRepo).should().tag(eq("v1.0.0.RELEASE")); + then(this.gitRepo).should().pushTag(eq("v1.0.0.RELEASE")); } @Test public void should_commit_when_snapshot_version_is_present_with_post_release_msg() { this.updater.commitAfterBumpingVersions(this.file, projectVersion("1.0.0.BUILD-SNAPSHOT")); - then(this.gitRepo).should().commit(any(File.class), eq("Bumping versions to 1.0.1.BUILD-SNAPSHOT after release")); - then(this.gitRepo).should(never()).tag(any(File.class), anyString()); + then(this.gitRepo).should().commit(eq("Bumping versions to 1.0.1.BUILD-SNAPSHOT after release")); + then(this.gitRepo).should(never()).tag(anyString()); } @Test public void should_not_commit_when_non_snapshot_version_is_present() { this.updater.commitAfterBumpingVersions(this.file, projectVersion("1.0.0.RELEASE")); - then(this.gitRepo).should(never()).commit(any(File.class), eq("Bumping versions after release")); - then(this.gitRepo).should(never()).tag(any(File.class), anyString()); + then(this.gitRepo).should(never()).commit(eq("Bumping versions after release")); + then(this.gitRepo).should(never()).tag(anyString()); } @Test public void should_not_revert_changes_for_snapshots() { this.updater.revertChangesIfApplicable(this.file, projectVersion("1.0.0.BUILD-SNAPSHOT")); - then(this.gitRepo).should(never()).revert(any(File.class), anyString()); + then(this.gitRepo).should(never()).revert(anyString()); } @Test public void should_revert_changes_when_version_is_not_snapshot() { this.updater.revertChangesIfApplicable(this.file, projectVersion("1.0.0.RELEASE")); - then(this.gitRepo).should().revert(any(File.class), eq("Going back to snapshots")); + then(this.gitRepo).should().revert(eq("Going back to snapshots")); } @Test public void should_push_current_branch() { this.updater.pushCurrentBranch(this.file); - then(this.gitRepo).should().pushCurrentBranch(any(File.class)); + then(this.gitRepo).should().pushCurrentBranch(); + } + + @Test + public void should_throw_exception_when_no_fixed_version_passed_for_the_project() { + BDDAssertions.thenThrownBy(() -> this.updater + .cloneProjectFromOrg("spring-cloud-sleuth")) + .isInstanceOf(IllegalStateException.class) + .hasMessageContaining("You haven't provided a version"); + } + + @Test + public void should_not_check_out_a_branch_if_it_does_not_exist_when_cloning_from_org() { + this.properties.getFixedVersions().put("spring-cloud-sleuth", "2.3.4.RELEASE"); + given(this.gitRepo.hasBranch(anyString())).willReturn(true); + given(this.gitRepo.hasBranch("2.3.x")).willReturn(false); + + this.updater.cloneProjectFromOrg("spring-cloud-sleuth"); + + then(this.gitRepo).should(never()).checkout(anyString()); + } + + @Test + public void should_check_out_a_branch_if_it_exists_when_cloning_from_org() { + this.properties.getFixedVersions().put("spring-cloud-sleuth", "2.3.4.RELEASE"); + given(this.gitRepo.hasBranch(anyString())).willReturn(false); + given(this.gitRepo.hasBranch("2.3.x")).willReturn(true); + + this.updater.cloneProjectFromOrg("spring-cloud-sleuth"); + + then(this.gitRepo).should().checkout("2.3.x"); + } + + @Test + public void should_check_out_a_branch_if_it_exists_when_cloning_from_org_and_its_a_release_train_version() { + this.properties.getFixedVersions().put("spring-cloud-release", "Finchley.SR6"); + given(this.gitRepo.hasBranch(anyString())).willReturn(false); + given(this.gitRepo.hasBranch("Finchley.x")).willReturn(true); + + this.updater.cloneProjectFromOrg("spring-cloud-release"); + + then(this.gitRepo).should().checkout("Finchley.x"); } private ProjectVersion projectVersion(String version) { diff --git a/spring-cloud-release-tools-core/src/test/java/org/springframework/cloud/release/internal/pom/ProjectsTests.java b/spring-cloud-release-tools-core/src/test/java/org/springframework/cloud/release/internal/pom/ProjectsTests.java index 93ee42fc..9507594b 100644 --- a/spring-cloud-release-tools-core/src/test/java/org/springframework/cloud/release/internal/pom/ProjectsTests.java +++ b/spring-cloud-release-tools-core/src/test/java/org/springframework/cloud/release/internal/pom/ProjectsTests.java @@ -35,6 +35,15 @@ public class ProjectsTests { then(projects.forName("spring-cloud-starter-build").version).isEqualTo("1.0.0"); } + @Test + public void should_return_true_when_a_project_by_name_exists() { + Set projectVersions = new HashSet<>(); + projectVersions.add(new ProjectVersion("spring-cloud-starter-build", "1.0.0")); + Projects projects = new Projects(projectVersions); + + then(projects.containsProject("spring-cloud-starter-build")).isTrue(); + } + @Test public void should_find_projects_starting_with_name() { Set projectVersions = new HashSet<>(); diff --git a/spring-cloud-release-tools-core/src/test/java/org/springframework/cloud/release/internal/pom/PropertyStorerTests.java b/spring-cloud-release-tools-core/src/test/java/org/springframework/cloud/release/internal/pom/PropertyStorerTests.java index 3475ddb7..f111d01a 100644 --- a/spring-cloud-release-tools-core/src/test/java/org/springframework/cloud/release/internal/pom/PropertyStorerTests.java +++ b/spring-cloud-release-tools-core/src/test/java/org/springframework/cloud/release/internal/pom/PropertyStorerTests.java @@ -2,14 +2,12 @@ package org.springframework.cloud.release.internal.pom; import org.apache.maven.plugin.logging.Log; import org.codehaus.mojo.versions.rewriting.ModifiedPomXMLEventReader; -import org.hamcrest.Description; -import org.hamcrest.TypeSafeMatcher; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.BDDMockito; import org.mockito.InjectMocks; import org.mockito.Mock; -import org.mockito.runners.MockitoJUnitRunner; +import org.mockito.junit.MockitoJUnitRunner; import static org.mockito.BDDMockito.then; @@ -30,15 +28,8 @@ public class PropertyStorerTests { } private String containsWarnMsgAboutEmptyVersion() { - return BDDMockito.argThat(new TypeSafeMatcher() { - @Override protected boolean matchesSafely(String item) { - return item.contains("is empty. Will not set it"); - } - - @Override public void describeTo(Description description) { - - } - }); + return BDDMockito.argThat( + argument -> argument.contains("is empty. Will not set it")); } } \ No newline at end of file diff --git a/spring-cloud-release-tools-core/src/test/java/org/springframework/cloud/release/internal/pom/SCReleasePomParserTests.java b/spring-cloud-release-tools-core/src/test/java/org/springframework/cloud/release/internal/pom/SCReleasePomParserTests.java index fed65f0f..33c14868 100644 --- a/spring-cloud-release-tools-core/src/test/java/org/springframework/cloud/release/internal/pom/SCReleasePomParserTests.java +++ b/spring-cloud-release-tools-core/src/test/java/org/springframework/cloud/release/internal/pom/SCReleasePomParserTests.java @@ -50,6 +50,15 @@ public class SCReleasePomParserTests { .hasMessageContaining("The pom doesn't have a [spring-boot-starter-parent] artifact id"); } + @Test + public void should_populate_sc_release_version() { + SCReleasePomParser parser = new SCReleasePomParser(this.springCloudReleaseProject); + + String scReleaseVersion = parser.allVersions().versionForProject("spring-cloud-release"); + + then(scReleaseVersion).isEqualTo("Dalston.BUILD-SNAPSHOT"); + } + @Test public void should_populate_boot_version() { SCReleasePomParser parser = new SCReleasePomParser(this.springCloudReleaseProject); diff --git a/spring-cloud-release-tools-core/src/test/java/org/springframework/cloud/release/internal/sagan/SaganUpdaterTest.java b/spring-cloud-release-tools-core/src/test/java/org/springframework/cloud/release/internal/sagan/SaganUpdaterTest.java index a1e5f219..6b0f1bac 100644 --- a/spring-cloud-release-tools-core/src/test/java/org/springframework/cloud/release/internal/sagan/SaganUpdaterTest.java +++ b/spring-cloud-release-tools-core/src/test/java/org/springframework/cloud/release/internal/sagan/SaganUpdaterTest.java @@ -8,10 +8,11 @@ import org.hamcrest.TypeSafeMatcher; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; +import org.mockito.ArgumentMatcher; import org.mockito.BDDMockito; import org.mockito.InjectMocks; import org.mockito.Mock; -import org.mockito.runners.MockitoJUnitRunner; +import org.mockito.junit.MockitoJUnitRunner; import org.springframework.cloud.release.internal.pom.ProjectVersion; import static org.mockito.BDDMockito.then; @@ -103,23 +104,17 @@ public class SaganUpdaterTest { "http://cloud.spring.io/foo/1.1.x/", "SNAPSHOT"))); } - private TypeSafeMatcher> withReleaseUpdate(final String version, + private ArgumentMatcher> withReleaseUpdate(final String version, final String refDocUrl, final String releaseStatus) { - return new TypeSafeMatcher>() { - @Override protected boolean matchesSafely(List items) { - ReleaseUpdate item = items.get(0); + return argument -> { + ReleaseUpdate item = argument.get(0); return "foo".equals(item.artifactId) && releaseStatus.equals(item.releaseStatus) && version.equals(item.version) && refDocUrl.equals(item.apiDocUrl) && refDocUrl.equals(item.refDocUrl) && item.current; - } - - @Override public void describeTo(Description description) { - - } - }; + }; } } \ No newline at end of file diff --git a/spring-cloud-release-tools-spring/pom.xml b/spring-cloud-release-tools-spring/pom.xml index f81b4da9..83776369 100644 --- a/spring-cloud-release-tools-spring/pom.xml +++ b/spring-cloud-release-tools-spring/pom.xml @@ -36,6 +36,10 @@ 5.0.3 true + + com.fasterxml.jackson.dataformat + jackson-dataformat-yaml + 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 2ca026bc..9210705d 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 @@ -18,6 +18,7 @@ package org.springframework.cloud.release.internal; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.CommandLineRunner; import org.springframework.boot.SpringApplication; +import org.springframework.boot.WebApplicationType; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.cloud.release.internal.options.Options; import org.springframework.cloud.release.internal.options.Parser; @@ -28,7 +29,7 @@ public class ReleaserApplication implements CommandLineRunner { public static void main(String[] args) { SpringApplication application = new SpringApplication(ReleaserApplication.class); - application.setWebEnvironment(false); + application.setWebApplicationType(WebApplicationType.NONE); application.run(args); } 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 index f352eefb..80fe614c 100644 --- 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 @@ -7,14 +7,17 @@ import java.util.List; * @author Marcin Grzejszczak */ public class Options { - public Boolean fullRelease = true; - public Boolean interactive = true; - public List taskNames = new ArrayList<>(); + public Boolean metaRelease; + public Boolean fullRelease; + public Boolean interactive; + public List taskNames; public String startFrom = ""; public String range = ""; - Options(Boolean fullRelease, Boolean interactive, List taskNames, String startFrom, + Options(Boolean metaRelease, Boolean fullRelease, + Boolean interactive, List taskNames, String startFrom, String range) { + this.metaRelease = metaRelease; this.fullRelease = fullRelease; this.interactive = interactive; this.taskNames = taskNames; 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 index 0c018fc6..bece7b68 100644 --- 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 @@ -4,12 +4,18 @@ import java.util.ArrayList; import java.util.List; public class OptionsBuilder { + private Boolean metaRelease = false; private Boolean fullRelease = false; private Boolean interactive = true; private List taskNames = new ArrayList<>(); private String startFrom = ""; private String range = ""; + public OptionsBuilder metaRelease(Boolean metaRelease) { + this.metaRelease = metaRelease; + return this; + } + public OptionsBuilder fullRelease(Boolean fullRelease) { this.fullRelease = fullRelease; return this; @@ -36,7 +42,8 @@ public class OptionsBuilder { } public Options options() { - return new Options(this.fullRelease, this.interactive, this.taskNames, this.startFrom, + return new Options(this.metaRelease, 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/spring/Args.java b/spring-cloud-release-tools-spring/src/main/java/org/springframework/cloud/release/internal/spring/Args.java index 3e3b4274..443ce489 100644 --- 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 @@ -2,6 +2,7 @@ package org.springframework.cloud.release.internal.spring; import java.io.File; +import org.apache.commons.validator.Arg; import org.springframework.cloud.release.internal.Releaser; import org.springframework.cloud.release.internal.ReleaserProperties; import org.springframework.cloud.release.internal.pom.ProjectVersion; @@ -18,10 +19,11 @@ class Args { final ProjectVersion versionFromScRelease; final ReleaserProperties properties; final boolean interactive; + final TaskType taskType; Args(Releaser releaser, File project, Projects projects, ProjectVersion originalVersion, ProjectVersion versionFromScRelease, ReleaserProperties properties, - boolean interactive) { + boolean interactive, TaskType taskType) { this.releaser = releaser; this.project = project; this.projects = projects; @@ -29,5 +31,33 @@ class Args { this.versionFromScRelease = versionFromScRelease; this.properties = properties; this.interactive = interactive; + this.taskType = taskType; + } + + // Used by meta-release task + Args(Releaser releaser, Projects projects, + ProjectVersion versionFromScRelease, + ReleaserProperties properties, + boolean interactive) { + this.releaser = releaser; + this.project = null; + this.projects = projects; + this.originalVersion = null; + this.versionFromScRelease = versionFromScRelease; + this.properties = properties; + this.interactive = interactive; + this.taskType = TaskType.POST_RELEASE; + } + + // Used for tests + Args(TaskType taskType) { + this.releaser = null; + this.project = null; + this.projects = null; + this.originalVersion = null; + this.versionFromScRelease = null; + this.properties = null; + this.interactive = false; + this.taskType = taskType; } } 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 index 01b88cdb..2dc02590 100644 --- 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 @@ -23,15 +23,19 @@ class OptionsParser implements Parser { OptionParser parser = new OptionParser(); parser.allowsUnrecognizedOptions(); try { + ArgumentAcceptingOptionSpec metaReleaseOpt = parser + .acceptsAll(Arrays.asList("x", "meta-release"), + "Do you want to do the meta release?") + .withRequiredArg().ofType(Boolean.class).defaultsTo(false); ArgumentAcceptingOptionSpec fullReleaseOpt = parser .acceptsAll(Arrays.asList("f", "full-release"), - "Do you want to do the full release") + "Do you want to do the full release of a single project?") .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") + "Do you want to set the properties from the command line of a single project?") .withRequiredArg().ofType(Boolean.class).defaultsTo(true); - Tasks.ALL_TASKS.forEach(task -> + Tasks.NON_COMPOSITE_TASKS.forEach(task -> parser.acceptsAll(Arrays.asList(task.shortName, task.name), task.description) .withOptionalArg()); @@ -49,14 +53,16 @@ class OptionsParser implements Parser { printHelpMessage(parser); System.exit(0); } + Boolean metaRelease = options.valueOf(metaReleaseOpt); Boolean interactive = options.valueOf(interactiveOpt); Boolean fullRelease = options.has(fullReleaseOpt); - List taskNames = Tasks.ALL_TASKS.stream() + List taskNames = Tasks.NON_COMPOSITE_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() + .metaRelease(metaRelease) .fullRelease(fullRelease) .interactive(interactive) .taskNames(taskNames) @@ -94,7 +100,7 @@ class OptionsParser implements Parser { } private String intro() { - return "\nHere you can find the list of tasks in order\n\n[" + Tasks.tasksInOrder() + "]\n\n"; + return "\nHere you can find the list of tasks in order\n\n[" + Tasks.allTasksInOrder() + "]\n\n"; } private String examples() { 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 index d32e72a7..48ab06d3 100644 --- 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 @@ -23,7 +23,7 @@ class OptionsProcessor { private final List allTasks; OptionsProcessor(Releaser releaser, ReleaserProperties properties) { - this(releaser, properties, Tasks.ALL_TASKS); + this(releaser, properties, Tasks.ALL_TASKS_PER_PROJECT); } OptionsProcessor(Releaser releaser, ReleaserProperties properties, List allTasks) { @@ -33,26 +33,60 @@ class OptionsProcessor { } void processOptions(Options options, Args defaultArgs) { + processOptions(options, defaultArgs, this.allTasks); + } + + void processOptions(Options options, Args defaultArgs, List tasks) { Args args = args(defaultArgs, options.interactive); + if (args.taskType == TaskType.POST_RELEASE) { + String chosenOption = chosenOption(); + int pickedInteger = Integer.parseInt(chosenOption); + boolean pickedOptionIsComposite = pickedInteger <= 1; + boolean pickedOptionIsFromPostRelease = pickedInteger >= Tasks.ALL_TASKS_PER_PROJECT.size() + - Tasks.DEFAULT_TASKS_PER_RELEASE.size(); + if (options.fullRelease || pickedOptionIsComposite) { + postReleaseTask().execute(args); + } else if (pickedOptionIsFromPostRelease) { + processNonComposite(options, tasks, args); + } else { + log.info("Picked option [{}] doesn't allow post release steps", pickedInteger); + } + return; + } 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 { + processNonComposite(options, tasks, args); + } + } + + private void processNonComposite(Options options, List tasks, Args args) { + if (StringUtils.hasText(options.startFrom)) { + startFrom(tasks, options, args); } else if (StringUtils.hasText(options.range)) { - range(options.range, args); + range(tasks, options.range, args); } else if (!options.taskNames.isEmpty()) { - tasks(options.taskNames, args); + tasks(tasks, options.taskNames, args); } else if (options.interactive) { - interactiveOnly(args); + interactiveOnly(tasks, args); } else { throw new IllegalStateException("You haven't picked any recognizable option"); } } + void postReleaseOptions(Options options, Args defaultArgs) { + Args args = args(defaultArgs, options.interactive); + processOptions(options, args); + } + + Task postReleaseTask() { + return Tasks.POST_RELEASE; + } + Task releaseTask() { return Tasks.RELEASE; } @@ -61,16 +95,18 @@ class OptionsProcessor { return Tasks.RELEASE_VERBOSE; } - private void interactiveOnly(Args defaultArgs) { - log.info(buildOptionsText().toString()); - executeTaskFromOption(defaultArgs); + private void interactiveOnly(List tasks, Args defaultArgs) { + if (defaultArgs.taskType != TaskType.POST_RELEASE) { + log.info(buildOptionsText().toString()); + } + executeTaskFromOption(tasks, defaultArgs); } - private void tasks(List taskNames, Args defaultArgs) { - Tasks.forNames(this.allTasks, taskNames).forEach(task -> task.execute(defaultArgs)); + private void tasks(List tasks, List taskNames, Args defaultArgs) { + Tasks.forNames(tasks, taskNames).forEach(task -> task.execute(defaultArgs)); } - private void range(String range, Args defaultArgs) { + private void range(List tasks, String range, Args defaultArgs) { String[] splitRange = range.split("-"); String start = splitRange[0]; String stop = ""; @@ -79,7 +115,7 @@ class OptionsProcessor { } boolean started = false; boolean sameRange = start.equals(stop); - for (Task task : this.allTasks) { + for (Task task : tasks) { if (start.equals(task.name) || start.equals(task.shortName)) { started = true; task.execute(defaultArgs); @@ -95,9 +131,9 @@ class OptionsProcessor { } } - private void startFrom(Options options, Args defaultArgs) { + private void startFrom(List tasks, Options options, Args defaultArgs) { boolean started = false; - for (Task task : this.allTasks) { + for (Task task : tasks) { if (options.startFrom.equals(task.name) || options.startFrom.equals(task.shortName)) { started = true; task.execute(defaultArgs); @@ -120,25 +156,25 @@ class OptionsProcessor { return msg; } - void executeTaskFromOption(Args defaultArgs) { + void executeTaskFromOption(List tasks, Args defaultArgs) { String input = chosenOption(); switch (input.toLowerCase()) { case "q": System.exit(0); default: if (input.contains("-")) { - rangeInteractive(defaultArgs, input); + rangeInteractive(tasks, defaultArgs, input); } else if (input.contains(",")) { - tasksInteractive(defaultArgs, input); + tasksInteractive(tasks, defaultArgs, input); } else { - singleTask(defaultArgs, input); + singleTask(tasks, defaultArgs, input); } } } - private void singleTask(Args defaultArgs, String input) { + private void singleTask(List tasks, Args defaultArgs, String input) { int chosenOption = Integer.parseInt(input); - Task task = this.allTasks.get(chosenOption); + Task task = tasks.get(chosenOption); boolean interactive = false; if (task == Tasks.RELEASE_VERBOSE) { interactive = true; @@ -147,31 +183,32 @@ class OptionsProcessor { task.execute(args(defaultArgs, interactive)); } - private void tasksInteractive(Args defaultArgs, String input) { - List tasks = Arrays.asList(input.split(",")); + private void tasksInteractive(List tasks, Args defaultArgs, String input) { + List tasksFromInput = Arrays.asList(input.split(",")); List taskNames = new ArrayList<>(); - for (String task : tasks) { + for (String task : tasksFromInput) { Integer taskIndex = Integer.valueOf(task); - taskNames.add(this.allTasks.get(taskIndex).name); + taskNames.add(tasks.get(taskIndex).name); } - tasks(taskNames, defaultArgs); + tasks(tasks, taskNames, defaultArgs); } - private void rangeInteractive(Args defaultArgs, String input) { + private void rangeInteractive(List tasks, 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); + String firstName = tasks.get(start).name; + String second = stop != null ? tasks.get(stop).name : ""; + range(tasks, 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); + defaultArgs.originalVersion, defaultArgs.versionFromScRelease, + this.properties, interactive, defaultArgs.taskType); } String chosenOption() { 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 fa890904..badd4e95 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 @@ -15,18 +15,21 @@ */ package org.springframework.cloud.release.internal.spring; +import org.springframework.beans.factory.annotation.Autowired; 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.docs.DocumentationUpdater; import org.springframework.cloud.release.internal.gradle.GradleUpdater; import org.springframework.cloud.release.internal.options.Parser; +import org.springframework.cloud.release.internal.sagan.Release; import org.springframework.cloud.release.internal.sagan.SaganClient; import org.springframework.cloud.release.internal.sagan.SaganUpdater; import org.springframework.cloud.release.internal.template.TemplateGenerator; import org.springframework.cloud.release.internal.project.ProjectBuilder; import org.springframework.cloud.release.internal.git.ProjectGitHandler; import org.springframework.cloud.release.internal.pom.ProjectPomUpdater; +import org.springframework.context.ApplicationContext; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @@ -34,14 +37,37 @@ import org.springframework.context.annotation.Configuration; @EnableConfigurationProperties(ReleaserProperties.class) class ReleaserConfiguration { - @Bean SpringReleaser releaser(ReleaserProperties properties, SaganClient saganClient) { - ProjectPomUpdater pomUpdater = new ProjectPomUpdater(properties); - ProjectGitHandler handler = new ProjectGitHandler(properties); - SaganUpdater saganUpdater = new SaganUpdater(saganClient); - return new SpringReleaser(new Releaser(pomUpdater, new ProjectBuilder(properties), - handler, new TemplateGenerator(properties, handler), - new GradleUpdater(properties), saganUpdater, - new DocumentationUpdater(handler)), properties); + @Autowired ReleaserProperties properties; + + @Bean SpringReleaser springReleaser(Releaser releaser, + ReleaserPropertiesUpdater updater) { + return new SpringReleaser(releaser, this.properties, updater); + } + + @Bean ProjectBuilder projectBuilder() { return new ProjectBuilder(this.properties); } + + @Bean ProjectPomUpdater pomUpdater() { return new ProjectPomUpdater(this.properties); } + + @Bean ProjectGitHandler projectGitHandler() { return new ProjectGitHandler(this.properties); } + + @Bean TemplateGenerator templateGenerator(ProjectGitHandler handler) { return new TemplateGenerator(this.properties, handler); } + + @Bean GradleUpdater gradleUpdater() { return new GradleUpdater(this.properties); } + + @Bean SaganUpdater saganUpdater(SaganClient saganClient) { return new SaganUpdater(saganClient); } + + @Bean DocumentationUpdater documentationUpdater(ProjectGitHandler handler) { return new DocumentationUpdater(handler); } + + @Bean Releaser releaser(ProjectPomUpdater projectPomUpdater, ProjectBuilder projectBuilder, + ProjectGitHandler projectGitHandler, TemplateGenerator templateGenerator, + GradleUpdater gradleUpdater, SaganUpdater saganUpdater, + DocumentationUpdater documentationUpdater) { + return new Releaser(projectPomUpdater, projectBuilder, projectGitHandler, + templateGenerator, gradleUpdater, saganUpdater, documentationUpdater); + } + + @Bean ReleaserPropertiesUpdater releaserPropertiesUpdater(ApplicationContext context) { + return new ReleaserPropertiesUpdater(context); } @Bean Parser optionsParser() { diff --git a/spring-cloud-release-tools-spring/src/main/java/org/springframework/cloud/release/internal/spring/ReleaserPropertiesUpdater.java b/spring-cloud-release-tools-spring/src/main/java/org/springframework/cloud/release/internal/spring/ReleaserPropertiesUpdater.java new file mode 100644 index 00000000..72caca8a --- /dev/null +++ b/spring-cloud-release-tools-spring/src/main/java/org/springframework/cloud/release/internal/spring/ReleaserPropertiesUpdater.java @@ -0,0 +1,24 @@ +package org.springframework.cloud.release.internal.spring; + +import java.util.Map; + +import org.springframework.cloud.release.internal.ReleaserProperties; +import org.springframework.cloud.release.internal.ReleaserPropertiesAware; +import org.springframework.context.ApplicationContext; + +/** + * @author Marcin Grzejszczak + */ +class ReleaserPropertiesUpdater { + private final ApplicationContext context; + + public ReleaserPropertiesUpdater(ApplicationContext context) { + this.context = context; + } + + public void updateProperties(ReleaserProperties properties) { + Map beans = this.context + .getBeansOfType(ReleaserPropertiesAware.class); + beans.values().forEach(aware -> aware.setReleaserProperties(properties)); + } +} 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 0b6e4861..77081c8a 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 @@ -1,20 +1,23 @@ package org.springframework.cloud.release.internal.spring; import java.io.File; -import java.lang.invoke.MethodHandles; +import java.io.IOException; +import java.util.ArrayList; import java.util.List; import java.util.stream.Collectors; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.dataformat.yaml.YAMLFactory; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.BeanUtils; 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 org.springframework.cloud.release.internal.sagan.Project; +import org.springframework.util.StringUtils; /** * Releaser that gets input from console @@ -22,24 +25,28 @@ import org.springframework.cloud.release.internal.sagan.Project; * @author Marcin Grzejszczak */ public class SpringReleaser { - private static final Logger log = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass()); + private static final Logger log = LoggerFactory.getLogger(SpringReleaser.class); private final Releaser releaser; private final ReleaserProperties properties; private final OptionsProcessor optionsProcessor; + private final ReleaserPropertiesUpdater updater; + private final ObjectMapper objectMapper = new ObjectMapper(new YAMLFactory()); - @Autowired - public SpringReleaser(Releaser releaser, ReleaserProperties properties) { + public SpringReleaser(Releaser releaser, ReleaserProperties properties, + ReleaserPropertiesUpdater updater) { this.releaser = releaser; this.properties = properties; + this.updater = updater; this.optionsProcessor = new OptionsProcessor(releaser, properties); } SpringReleaser(Releaser releaser, ReleaserProperties properties, - OptionsProcessor optionsProcessor) { + OptionsProcessor optionsProcessor, ReleaserPropertiesUpdater updater) { this.releaser = releaser; this.properties = properties; this.optionsProcessor = optionsProcessor; + this.updater = updater; } /** @@ -50,17 +57,108 @@ public class SpringReleaser { } public void release(Options options) { + ProjectsAndVersion projectsAndVersion = null; + // if meta release, first clone, then continue as usual + if (options.metaRelease) { + log.info("Meta Release picked. Will iterate over all projects and perform release of each one"); + this.properties.getGit().setFetchVersionsFromGit(false); + metaReleaseProjects(options).forEach(project -> { + processProjectForMetaRelease(options, project); + }); + } else { + log.info("Single project release picked. Will release only the current project"); + File projectFolder = projectFolder(); + projectsAndVersion = processProject(options, projectFolder, TaskType.RELEASE); + } + this.optionsProcessor.postReleaseOptions(options, postReleaseOptionsAgs(options, projectsAndVersion)); + } + + private void processProjectForMetaRelease(Options options, String project) { + File clonedProjectFromOrg = this.releaser.clonedProjectFromOrg(project); + ReleaserProperties copy = clonePropertiesForProject(); + updatePropertiesIfCustomConfigPresent(copy, clonedProjectFromOrg); + log.info("Successfully cloned the project [{}] to [{}]", project, clonedProjectFromOrg); + try { + processProject(options, clonedProjectFromOrg, TaskType.RELEASE); + } catch (Exception e) { + log.error("\n\n\nBUILD FAILED!!!\n\nException occurred for project <" + + project + "> \n\n"); + throw e; + } + } + + private ReleaserProperties clonePropertiesForProject() { + ReleaserProperties copy = new ReleaserProperties(); + BeanUtils.copyProperties(this.properties, copy); + return copy; + } + + private void updatePropertiesIfCustomConfigPresent(ReleaserProperties copy, + File clonedProjectFromOrg) { + File releaserConfig = new File(clonedProjectFromOrg, "config/releaser.yml"); + if (releaserConfig.exists()) { + try { + ReleaserProperties releaserProperties = this.objectMapper + .readValue(releaserConfig, ReleaserProperties.class); + log.info("config/releaser.yml found. Will update the current properties"); + copy.setMaven(releaserProperties.getMaven()); + copy.setGradle(releaserProperties.getGradle()); + } + catch (IOException e) { + throw new IllegalStateException(e); + } + } + log.info("Updating working directory to [{}]", clonedProjectFromOrg.getAbsolutePath()); + copy.setWorkingDir(clonedProjectFromOrg.getAbsolutePath()); + this.updater.updateProperties(copy); + } + + private List metaReleaseProjects(Options options) { + List projects = new ArrayList<>(this.properties.getFixedVersions().keySet()); + if (StringUtils.hasText(options.startFrom)) { + int projectIndex = projects.indexOf(options.startFrom); + if (projectIndex < 0) throw new IllegalStateException("Project [" + options.startFrom + "] not found"); + projects = projects.subList(projectIndex, projects.size()); + options.startFrom = ""; + } else if (!options.taskNames.isEmpty()) { + projects = projects.stream() + .filter(project -> options.taskNames.contains(project)) + .collect(Collectors.toList()); + options.taskNames = new ArrayList<>(); + } + log.info("\n\n\nFor meta-release, will release the projects {}\n\n\n", projects); + return projects; + } + + private File projectFolder() { String workingDir = this.properties.getWorkingDir(); - File project = new File(workingDir); - ProjectVersion originalVersion = new ProjectVersion(project); + return new File(workingDir); + } + + Args postReleaseOptionsAgs(Options options, ProjectsAndVersion projectsAndVersion) { + Projects projects = projectsAndVersion == null ? + projectsToUpdateForFixedVersions() : projectsAndVersion.projectVersions; + ProjectVersion version = projects.containsProject(this.properties.getMetaRelease().getReleaseTrainProjectName()) ? + projects.forName(this.properties.getMetaRelease().getReleaseTrainProjectName()) : versionFromBranch(); + return new Args(this.releaser, projects, version, + this.properties, options.interactive); + } + + private ProjectVersion versionFromBranch() { + String branch = this.properties.getPom().getBranch(); + return new ProjectVersion(projectFolder().getName(), branch.startsWith("v") ? branch.substring(1) : branch); + } + + private ProjectsAndVersion projects(File project) { ProjectVersion versionFromScRelease; Projects projectsToUpdate; - if (this.properties.getGit().isFetchVersionsFromGit()) { + if (this.properties.getGit().isFetchVersionsFromGit() && !this.properties.getMetaRelease().isEnabled()) { printVersionRetrieval(); projectsToUpdate = this.releaser.retrieveVersionsFromSCRelease(); versionFromScRelease = projectsToUpdate.forFile(project); assertNoSnapshotsForANonSnapshotProject(projectsToUpdate, versionFromScRelease); } else { + ProjectVersion originalVersion = new ProjectVersion(project); String fixedVersionForProject = this.properties.getFixedVersions().get(originalVersion.projectName); versionFromScRelease = new ProjectVersion(originalVersion.projectName, fixedVersionForProject == null ? originalVersion.version : fixedVersionForProject); @@ -70,9 +168,35 @@ public class SpringReleaser { projectsToUpdate.add(versionFromScRelease); printSettingVersionFromFixedVersions(projectsToUpdate); } - final Args defaultArgs = new Args(this.releaser, project, projectsToUpdate, - originalVersion, versionFromScRelease, this.properties, options.interactive); + return new ProjectsAndVersion(projectsToUpdate, versionFromScRelease); + } + + class ProjectsAndVersion { + final Projects projectVersions; + final ProjectVersion versionFromScRelease; + + ProjectsAndVersion(Projects projectVersions, ProjectVersion versionFromScRelease) { + this.projectVersions = projectVersions; + this.versionFromScRelease = versionFromScRelease; + } + } + + private ProjectsAndVersion processProject(Options options, File project, TaskType taskType) { + ProjectsAndVersion projectsAndVersion = projects(project); + ProjectVersion originalVersion = new ProjectVersion(project); + final Args defaultArgs = new Args(this.releaser, project, projectsAndVersion.projectVersions, + originalVersion, projectsAndVersion.versionFromScRelease, this.properties, + options.interactive, taskType); this.optionsProcessor.processOptions(options, defaultArgs); + return projectsAndVersion; + } + + private Projects projectsToUpdateForFixedVersions() { + Projects projectsToUpdate = this.properties.getFixedVersions().entrySet().stream() + .map(entry -> new ProjectVersion(entry.getKey(), entry.getValue())) + .distinct().collect(Collectors.toCollection(Projects::new)); + printSettingVersionFromFixedVersions(projectsToUpdate); + return projectsToUpdate; } private void printVersionRetrieval() { 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 index 7eb28036..a7dc5809 100644 --- 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 @@ -18,17 +18,30 @@ class Task { final String shortName; final String header; final String description; + final TaskType taskType; private final Consumer consumer; - Task(String name, String shortName, String header, String description, Consumer consumer) { + Task(String name, String shortName, String header, String description, + Consumer consumer) { + this(name, shortName, header, description, consumer, TaskType.RELEASE); + } + + Task(String name, String shortName, String header, String description, + Consumer consumer, TaskType taskType) { this.name = name; this.shortName = shortName; this.header = header; this.description = description; this.consumer = consumer; + this.taskType = taskType; } void execute(Args args) { + if (args.taskType != this.taskType) { + log.info("Skipping [{}] since task type is [{}] and should be [{}]]", + this.name, this.taskType, args.taskType); + return; + } try { boolean interactive = args.interactive; printLog(interactive); @@ -48,7 +61,7 @@ class Task { } private void printLog(boolean interactive) { - log.info("\n\n\n=== {} ===\n\n{} {}\n\n", header, description, interactive ? MSG : ""); + log.info("\n\n\n=== {} ===\n\n{} {}\n\n", header, description, interactive ? MSG : ""); } boolean skipStep() { @@ -67,5 +80,4 @@ class Task { 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 index 08c608d7..efcc06e5 100644 --- 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 @@ -1,5 +1,6 @@ package org.springframework.cloud.release.internal.spring; +import java.util.ArrayList; import java.util.List; import java.util.function.Consumer; import java.util.stream.Collectors; @@ -51,13 +52,13 @@ class Tasks { args.releaser.createBlog(args.versionFromScRelease, args.projects); args.releaser.createTweet(args.versionFromScRelease); args.releaser.createReleaseNotes(args.versionFromScRelease, args.projects); - }); + },TaskType.POST_RELEASE); static Task UPDATE_GUIDES = task("updateGuides", "ug", "UPDATE GUIDES", "Updating Spring Guides", args -> { args.releaser.updateSpringGuides(args.versionFromScRelease, args.projects); - }); + },TaskType.POST_RELEASE); static Task UPDATE_SAGAN = task("updateSagan", "g", "UPDATE SAGAN", "Updating Sagan with release info", @@ -69,9 +70,9 @@ class Tasks { "Updating documentation repository", args -> { args.releaser.updateDocumentationRepository(args.properties, args.versionFromScRelease); - }); + },TaskType.POST_RELEASE); - static final List DEFAULT_TASKS = Stream.of( + static final List DEFAULT_TASKS_PER_PROJECT = Stream.of( Tasks.UPDATING_POMS, Tasks.BUILD_PROJECT, Tasks.COMMIT, @@ -80,32 +81,61 @@ class Tasks { Tasks.SNAPSHOTS, Tasks.PUSH, Tasks.CLOSE_MILESTONE, + Tasks.UPDATE_SAGAN + ).collect(Collectors.toList()); + + static final List DEFAULT_TASKS_PER_RELEASE = Stream.of( Tasks.CREATE_TEMPLATES, - Tasks.UPDATE_SAGAN, Tasks.UPDATE_GUIDES, Tasks.UPDATE_DOCUMENTATION ).collect(Collectors.toList()); - static Task RELEASE = Tasks.task("release", "r", + static final List NON_COMPOSITE_TASKS = new ArrayList() { + { + addAll(DEFAULT_TASKS_PER_PROJECT); + addAll(DEFAULT_TASKS_PER_RELEASE); + } + }; + + static Task RELEASE = Tasks.task("release", "fr", "FULL RELEASE", "Perform a full release of this project without interruptions", - args -> DEFAULT_TASKS.forEach(task -> task.execute(args))); + args -> DEFAULT_TASKS_PER_PROJECT.forEach(task -> task.execute(args))); + static Task POST_RELEASE = Tasks.task("post-release", "pr", + "POST RELEASE TASKS", + "Perform post release tasks for this release without interruptions", + args -> DEFAULT_TASKS_PER_RELEASE.forEach(task -> task.execute(args)), + TaskType.POST_RELEASE); 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))); + args -> DEFAULT_TASKS_PER_PROJECT.forEach(task -> task.execute(args))); + static Task META_RELEASE = Tasks.task("meta-release", "x", + "META RELEASE", + "Perform a meta release of projects", + args -> DEFAULT_TASKS_PER_PROJECT.forEach(task -> { + args.properties.getMetaRelease().setEnabled(true); + task.execute(args); + })); static final List COMPOSITE_TASKS = Stream.of( RELEASE, - RELEASE_VERBOSE + RELEASE_VERBOSE, + META_RELEASE ).collect(Collectors.toList()); - static final List ALL_TASKS = Stream.of( - COMPOSITE_TASKS, DEFAULT_TASKS + static final List ALL_TASKS_PER_PROJECT = Stream.of( + COMPOSITE_TASKS, DEFAULT_TASKS_PER_PROJECT, DEFAULT_TASKS_PER_RELEASE ).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 Task task(String name, String shortName, String header, String description, + Consumer function) { + return task(name, shortName, header, description, function, TaskType.RELEASE); + } + + static Task task(String name, String shortName, String header, String description, + Consumer function, TaskType taskType) { + return new Task(name, shortName, header, description, function, taskType); } static List forNames(List tasks, List names) { @@ -114,7 +144,11 @@ class Tasks { .collect(Collectors.toList()); } - static String tasksInOrder() { - return DEFAULT_TASKS.stream().map(task -> task.name).collect(Collectors.joining(",")); + static String allTasksInOrder() { + return ALL_TASKS_PER_PROJECT.stream().map(task -> task.name).collect(Collectors.joining(",")); } } + +enum TaskType { + RELEASE, POST_RELEASE +} \ No newline at end of file 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 8fdc2184..7a81162b 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 @@ -7,8 +7,12 @@ import java.io.File; import java.io.IOException; import java.net.URISyntaxException; import java.nio.file.Files; +import java.util.ArrayList; import java.util.Arrays; +import java.util.HashMap; import java.util.Iterator; +import java.util.List; +import java.util.Map; import org.apache.maven.model.Model; import org.assertj.core.api.BDDAssertions; @@ -20,12 +24,15 @@ import org.junit.Test; import org.junit.rules.TemporaryFolder; import org.mockito.BDDMockito; import org.mockito.Mockito; +import org.springframework.boot.test.rule.OutputCapture; import org.springframework.cloud.release.internal.Releaser; import org.springframework.cloud.release.internal.ReleaserProperties; import org.springframework.cloud.release.internal.docs.DocumentationUpdater; import org.springframework.cloud.release.internal.git.GitTestUtils; import org.springframework.cloud.release.internal.git.ProjectGitHandler; import org.springframework.cloud.release.internal.gradle.GradleUpdater; +import org.springframework.cloud.release.internal.options.Options; +import org.springframework.cloud.release.internal.options.OptionsBuilder; import org.springframework.cloud.release.internal.pom.ProjectPomUpdater; import org.springframework.cloud.release.internal.pom.ProjectVersion; import org.springframework.cloud.release.internal.pom.Projects; @@ -37,6 +44,7 @@ import org.springframework.cloud.release.internal.sagan.Release; import org.springframework.cloud.release.internal.sagan.SaganClient; import org.springframework.cloud.release.internal.sagan.SaganUpdater; import org.springframework.cloud.release.internal.template.TemplateGenerator; +import org.springframework.context.ApplicationContext; import org.springframework.util.FileSystemUtils; /** @@ -45,13 +53,20 @@ import org.springframework.util.FileSystemUtils; public class AcceptanceTests { @Rule public TemporaryFolder tmp = new TemporaryFolder(); + @Rule public OutputCapture capture = new OutputCapture(); TestPomReader testPomReader = new TestPomReader(); File springCloudConsulProject; File temporaryFolder; File documentationFolder; TestProjectGitHandler gitHandler; + NonAssertingTestProjectGitHandler nonAssertingGitHandler; SaganClient saganClient = Mockito.mock(SaganClient.class); ReleaserProperties releaserProperties; + TemplateGenerator templateGenerator; + SaganUpdater saganUpdater; + DocumentationUpdater documentationUpdater; + ApplicationContext applicationContext = Mockito.mock(ApplicationContext.class); + ReleaserPropertiesUpdater updater = new ReleaserPropertiesUpdater(this.applicationContext); @Before public void setup() throws Exception { @@ -105,6 +120,7 @@ public class AcceptanceTests { SpringReleaser releaser = templateOnlyReleaser(project, "spring-cloud-consul", "vCamden.SR5", "1.1.2.RELEASE"); this.releaserProperties.getGit().setFetchVersionsFromGit(false); + this.releaserProperties.getFixedVersions().put("spring-cloud-release", "Finchley.RELEASE"); this.releaserProperties.getFixedVersions().put("spring-cloud-consul", "2.3.4.RELEASE"); File temporaryDestination = tmp.newFolder(); this.releaserProperties.getGit().setCloneDestinationDir(temporaryDestination.getAbsolutePath()); @@ -164,6 +180,80 @@ public class AcceptanceTests { .contains("Camden.SR5"); } + @Test + public void should_perform_a_meta_release_of_sc_release_and_consul() throws Exception { + // simulates an org + Map versions = new HashMap<>(); + versions.put("spring-cloud-release", "Camden.BUILD-SNAPSHOT"); + versions.put("spring-cloud-consul", "1.1.2.BUILD-SNAPSHOT"); + SpringReleaser releaser = metaReleaser(versions); + + releaser.release(new OptionsBuilder().metaRelease(true).options()); + + then(this.nonAssertingGitHandler.clonedProjects).hasSize(2); + this.nonAssertingGitHandler.clonedProjects + .forEach(project -> { + then(Arrays.asList("spring-cloud-starter-build", + "spring-cloud-consul")).contains(pom(project).getArtifactId()); + then(capture.toString()).contains("executed_build", "executed_deploy", "executed_docs"); + }); + BDDMockito.then(saganUpdater).should(BDDMockito.atLeastOnce()).updateSagan(BDDMockito.anyString(), + BDDMockito.any(ProjectVersion.class), BDDMockito.any(ProjectVersion.class)); + BDDMockito.then(documentationUpdater).should() + .updateDocsRepo(BDDMockito.any(ProjectVersion.class), BDDMockito.anyString()); + } + + @Test + public void should_perform_a_meta_release_of_consul_only_when_run_from_got_passed() throws Exception { + // simulates an org + Map versions = new HashMap<>(); + versions.put("spring-cloud-release", "Camden.BUILD-SNAPSHOT"); + versions.put("spring-cloud-build", "1.1.2.BUILD-SNAPSHOT"); + versions.put("spring-cloud-consul", "1.1.2.BUILD-SNAPSHOT"); + SpringReleaser releaser = metaReleaser(versions); + + releaser.release(new OptionsBuilder().metaRelease(true) + .startFrom("spring-cloud-consul") + .options()); + + then(this.nonAssertingGitHandler.clonedProjects).hasSize(1); + this.nonAssertingGitHandler.clonedProjects + .forEach(project -> { + then(pom(project).getArtifactId()).isEqualTo("spring-cloud-consul"); + then(capture.toString()).contains("executed_build", "executed_deploy", "executed_docs"); + }); + BDDMockito.then(saganUpdater).should(BDDMockito.atLeastOnce()).updateSagan(BDDMockito.anyString(), + BDDMockito.any(ProjectVersion.class), BDDMockito.any(ProjectVersion.class)); + BDDMockito.then(documentationUpdater).should() + .updateDocsRepo(BDDMockito.any(ProjectVersion.class), BDDMockito.anyString()); + } + + @Test + public void should_perform_a_meta_release_of_build_and_consul_only_when_task_names_got_passed() throws Exception { + // simulates an org + Map versions = new HashMap<>(); + versions.put("spring-cloud-release", "Camden.BUILD-SNAPSHOT"); + versions.put("spring-cloud-build", "1.1.2.BUILD-SNAPSHOT"); + versions.put("spring-cloud-consul", "1.1.2.BUILD-SNAPSHOT"); + SpringReleaser releaser = metaReleaser(versions); + + releaser.release(new OptionsBuilder().metaRelease(true) + .taskNames(Arrays.asList("spring-cloud-build", "spring-cloud-consul")) + .options()); + + then(this.nonAssertingGitHandler.clonedProjects).hasSize(2); + this.nonAssertingGitHandler.clonedProjects + .forEach(project -> { + then(Arrays.asList("spring-cloud-build", + "spring-cloud-consul")).contains(pom(project).getArtifactId()); + then(capture.toString()).contains("executed_build", "executed_deploy", "executed_docs"); + }); + BDDMockito.then(saganUpdater).should(BDDMockito.atLeastOnce()).updateSagan(BDDMockito.anyString(), + BDDMockito.any(ProjectVersion.class), BDDMockito.any(ProjectVersion.class)); + BDDMockito.then(documentationUpdater).should() + .updateDocsRepo(BDDMockito.any(ProjectVersion.class), BDDMockito.anyString()); + } + // issue #74 @Test public void should_perform_a_release_of_sc_build() throws Exception { @@ -366,6 +456,11 @@ public class AcceptanceTests { return releaserWithFullDeployment(expectedVersion, projectName, properties); } + private SpringReleaser metaReleaser(Map versions) throws Exception { + ReleaserProperties properties = metaReleaserProperties(versions); + return metaReleaserWithFullDeployment(properties); + } + private SpringReleaser releaserWithFullDeployment(String expectedVersion, String projectName, ReleaserProperties properties) throws Exception { Releaser releaser = defaultReleaser(expectedVersion, projectName, properties); @@ -373,7 +468,26 @@ public class AcceptanceTests { @Override String chosenOption() { return "0"; } - }); + + @Override void postReleaseOptions(Options options, Args defaultArgs) { + options.interactive = false; + super.postReleaseOptions(options, defaultArgs); + } + }, updater); + } + + private SpringReleaser metaReleaserWithFullDeployment(ReleaserProperties properties) throws Exception { + Releaser releaser = defaultMetaReleaser(properties); + return new SpringReleaser(releaser, properties, new OptionsProcessor(releaser, properties) { + @Override String chosenOption() { + return "0"; + } + + @Override void postReleaseOptions(Options options, Args defaultArgs) { + options.interactive = false; + super.postReleaseOptions(options, defaultArgs); + } + }, updater); } private SpringReleaser releaserWithSnapshotScRelease(File projectFile, String projectName, @@ -387,9 +501,14 @@ public class AcceptanceTests { Releaser releaser = defaultReleaser(expectedVersion, projectName, properties); return new SpringReleaser(releaser, properties, new OptionsProcessor(releaser, properties) { @Override String chosenOption() { - return "10"; + return "12"; } - }); + + @Override void postReleaseOptions(Options options, Args defaultArgs) { + options.interactive = true; + super.postReleaseOptions(options, defaultArgs); + } + }, updater); } private Releaser defaultReleaser(String expectedVersion, String projectName, @@ -415,15 +534,52 @@ public class AcceptanceTests { return releaser; } + private Releaser defaultMetaReleaser(ReleaserProperties properties) throws Exception { + ProjectPomUpdater pomUpdater = new ProjectPomUpdater(properties); + ProjectBuilder projectBuilder = new ProjectBuilder(properties); + NonAssertingTestProjectGitHandler handler = new NonAssertingTestProjectGitHandler(properties); + TemplateGenerator templateGenerator = Mockito.spy(new TemplateGenerator(properties, handler)); + GradleUpdater gradleUpdater = new GradleUpdater(properties); + SaganUpdater saganUpdater = Mockito.spy(new SaganUpdater(this.saganClient)); + DocumentationUpdater documentationUpdater = Mockito.spy(new DocumentationUpdater(handler) { + @Override public File updateDocsRepo(ProjectVersion currentProject, + String springCloudReleaseBranch) { + File file = super.updateDocsRepo(currentProject, springCloudReleaseBranch); + documentationFolder = file; + return file; + } + }); + Releaser releaser = Mockito.spy(new Releaser(pomUpdater, projectBuilder, handler, + templateGenerator, gradleUpdater, saganUpdater, documentationUpdater)); + this.nonAssertingGitHandler = handler; + this.templateGenerator = templateGenerator; + this.saganUpdater = saganUpdater; + this.documentationUpdater = documentationUpdater; + return releaser; + } + private ReleaserProperties releaserProperties(File project, String branch) throws URISyntaxException { ReleaserProperties releaserProperties = new ReleaserProperties(); releaserProperties.getGit().setSpringCloudReleaseGitUrl(file("/projects/spring-cloud-release/").toURI().getPath()); releaserProperties.getGit().setDocumentationUrl(file("/projects/spring-cloud-static-angel/").toURI().getPath()); - releaserProperties.getPom().setBranch(branch); - releaserProperties.setWorkingDir(project.getPath()); releaserProperties.getMaven().setBuildCommand("echo build"); releaserProperties.getMaven().setDeployCommand("echo deploy"); releaserProperties.getMaven().setPublishDocsCommands(new String[] { "echo docs"} ); + releaserProperties.setWorkingDir(project.getPath()); + releaserProperties.getPom().setBranch(branch); + this.releaserProperties = releaserProperties; + return releaserProperties; + } + + private ReleaserProperties metaReleaserProperties(Map versions) throws URISyntaxException { + ReleaserProperties releaserProperties = new ReleaserProperties(); + releaserProperties.getGit().setDocumentationUrl(file("/projects/spring-cloud-static-angel/").toURI().getPath()); + releaserProperties.getMaven().setBuildCommand("echo executed_build"); + releaserProperties.getMaven().setDeployCommand("echo executed_deploy"); + releaserProperties.getMaven().setPublishDocsCommands(new String[] { "echo executed_docs"} ); + releaserProperties.getMetaRelease().setGitOrgUrl("file://" + this.temporaryFolder.getAbsolutePath()); + releaserProperties.getMetaRelease().setEnabled(true); + releaserProperties.setFixedVersions(versions); this.releaserProperties = releaserProperties; return releaserProperties; } @@ -466,6 +622,48 @@ public class AcceptanceTests { } } + class NonAssertingTestProjectGitHandler extends ProjectGitHandler { + + boolean closedMilestones = false; + boolean issueCreatedInSpringGuides = false; + List clonedProjects = new ArrayList<>(); + + public NonAssertingTestProjectGitHandler(ReleaserProperties properties) { + super(properties); + } + + @Override public void closeMilestone(ProjectVersion releaseVersion) { + this.closedMilestones = true; + } + + @Override public void createIssueInSpringGuides(Projects projects, + ProjectVersion version) { + this.issueCreatedInSpringGuides = true; + } + + @Override public String milestoneUrl(ProjectVersion releaseVersion) { + return "http://foo.bar.com/" + releaseVersion.toString(); + } + + @Override public File cloneScReleaseProject() { + File file = super.cloneScReleaseProject(); + this.clonedProjects.add(file); + return file; + } + + @Override public File cloneDocumentationProject() { + File file = super.cloneDocumentationProject(); + this.clonedProjects.add(file); + return file; + } + + @Override public File cloneProjectFromOrg(String projectName) { + File file = super.cloneProjectFromOrg(projectName); + this.clonedProjects.add(file); + return file; + } + } + private File tmpFile(String relativePath) { return new File(this.temporaryFolder, relativePath); } 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 index 39875baf..9c0046bf 100644 --- 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 @@ -210,6 +210,10 @@ public class OptionsProcessorTests { @Override Task releaseTask() { return firstTask; } + + @Override String chosenOption() { + return "0"; + } }; Options options = nonInteractiveOpts().fullRelease(true).options(); @@ -226,6 +230,10 @@ public class OptionsProcessorTests { @Override Task releaseVerboseTask() { return firstTask; } + + @Override String chosenOption() { + return "0"; + } }; Options options = interactiveOpts().fullRelease(true).options(); @@ -245,7 +253,7 @@ public class OptionsProcessorTests { } private Args args() { - return new Args(null, null, null, null, null, null, false); + return new Args(null, null, null, null, null, null, false, TaskType.RELEASE); } private List list(String... list) { diff --git a/spring-cloud-release-tools-spring/src/test/java/org/springframework/cloud/release/internal/spring/ReleaserPropertiesIntegrationTests.java b/spring-cloud-release-tools-spring/src/test/java/org/springframework/cloud/release/internal/spring/ReleaserPropertiesIntegrationTests.java new file mode 100644 index 00000000..b8195534 --- /dev/null +++ b/spring-cloud-release-tools-spring/src/test/java/org/springframework/cloud/release/internal/spring/ReleaserPropertiesIntegrationTests.java @@ -0,0 +1,67 @@ +package org.springframework.cloud.release.internal.spring; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import org.assertj.core.api.BDDAssertions; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.BDDMockito; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.mock.mockito.SpyBean; +import org.springframework.cloud.release.internal.ReleaserApplication; +import org.springframework.cloud.release.internal.ReleaserProperties; +import org.springframework.cloud.release.internal.ReleaserPropertiesAware; +import org.springframework.context.ApplicationContext; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Import; +import org.springframework.test.context.junit4.SpringRunner; + +/** + * @author Marcin Grzejszczak + */ +@RunWith(SpringRunner.class) +@Import(ReleaserPropertiesIntegrationTests.Config.class) +public class ReleaserPropertiesIntegrationTests { + + @Autowired List propertiesAware; + @Autowired ApplicationContext context; + + @Test public void should_update_properties() { + ReleaserProperties properties = new ReleaserProperties(); + properties.getPom().setBranch("fooooo"); + + new ReleaserPropertiesUpdater(this.context).updateProperties(properties); + + BDDAssertions.then(this.propertiesAware).hasSize(2); + this.propertiesAware.forEach(aware -> + BDDAssertions.then(((ReleaserPropertiesHaving) aware) + .properties.getPom().getBranch()).isEqualTo("fooooo")); + } + + @Configuration + static class Config { + @Bean ReleaserPropertiesAware aware1() { + return new ReleaserPropertiesHaving(); + } + @Bean ReleaserPropertiesAware aware2() { + return new ReleaserPropertiesHaving(); + } + } + + static class ReleaserPropertiesHaving implements ReleaserPropertiesAware { + + ReleaserProperties properties; + + @Override public void setReleaserProperties(ReleaserProperties properties) { + this.properties = properties; + } + + ReleaserProperties getProps() { + return this.properties; + } + } +} \ No newline at end of file diff --git a/spring-cloud-release-tools-spring/src/test/java/org/springframework/cloud/release/internal/spring/ReleaserPropertiesUpdaterTests.java b/spring-cloud-release-tools-spring/src/test/java/org/springframework/cloud/release/internal/spring/ReleaserPropertiesUpdaterTests.java new file mode 100644 index 00000000..9cde5dd2 --- /dev/null +++ b/spring-cloud-release-tools-spring/src/test/java/org/springframework/cloud/release/internal/spring/ReleaserPropertiesUpdaterTests.java @@ -0,0 +1,48 @@ +package org.springframework.cloud.release.internal.spring; + +import java.util.HashMap; +import java.util.Map; + +import org.assertj.core.api.BDDAssertions; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.BDDMockito; +import org.springframework.cloud.release.internal.ReleaserProperties; +import org.springframework.cloud.release.internal.ReleaserPropertiesAware; +import org.springframework.context.ApplicationContext; + +import static org.junit.Assert.*; + +/** + * @author Marcin Grzejszczak + */ +public class ReleaserPropertiesUpdaterTests { + + ApplicationContext context = BDDMockito.mock(ApplicationContext.class); + + @Test public void should_update_properties() { + Aware aware = new Aware(); + BDDMockito.given(this.context.getBeansOfType(BDDMockito.any(Class.class))) + .willReturn(beansOfType(aware)); + ReleaserPropertiesUpdater updater = new ReleaserPropertiesUpdater(this.context); + + updater.updateProperties(new ReleaserProperties()); + + BDDAssertions.then(aware.properties).isNotNull(); + } + + private Map beansOfType(Aware aware) { + Map map = new HashMap<>(); + map.put("foo", aware); + return map; + } + + class Aware implements ReleaserPropertiesAware { + + ReleaserProperties properties; + + @Override public void setReleaserProperties(ReleaserProperties properties) { + this.properties = properties; + } + } +} \ No newline at end of file diff --git a/spring-cloud-release-tools-spring/src/test/java/org/springframework/cloud/release/internal/spring/TaskTests.java b/spring-cloud-release-tools-spring/src/test/java/org/springframework/cloud/release/internal/spring/TaskTests.java index cf3bbe33..974ef26f 100644 --- a/spring-cloud-release-tools-spring/src/test/java/org/springframework/cloud/release/internal/spring/TaskTests.java +++ b/spring-cloud-release-tools-spring/src/test/java/org/springframework/cloud/release/internal/spring/TaskTests.java @@ -10,7 +10,6 @@ import org.springframework.boot.test.rule.OutputCapture; import static org.assertj.core.api.BDDAssertions.then; import static org.assertj.core.api.BDDAssertions.thenThrownBy; -import static org.junit.Assert.*; /** * @author Marcin Grzejszczak @@ -27,21 +26,19 @@ public class TaskTests { } }); - task.execute(Mockito.mock(Args.class)); + task.execute(new Args(TaskType.RELEASE)); then(someBool.get()).isTrue(); } @Test public void should_fail_with_nice_text_on_exception() { final AtomicBoolean someBool = new AtomicBoolean(); - Task task = new Task("foo", "bar", "baz", "descr", new Consumer() { - @Override public void accept(Args args) { - someBool.set(true); - throw new RuntimeException("foooooooo"); - } + Task task = new Task("foo", "bar", "baz", "descr", args -> { + someBool.set(true); + throw new RuntimeException("foooooooo"); }); - thenThrownBy(() -> task.execute(Mockito.mock(Args.class))) + thenThrownBy(() -> task.execute(new Args(TaskType.RELEASE))) .isInstanceOf(RuntimeException.class); then(someBool.get()).isTrue(); then(this.capture.toString())