Closes milestone on Github

fixes #6
This commit is contained in:
Marcin Grzejszczak
2017-03-13 18:12:05 +01:00
parent d727a89d7a
commit 27f148ee43
13 changed files with 322 additions and 14 deletions

View File

@@ -35,6 +35,7 @@ why this tool makes it easy to automate the release / dependency update process
- Runs the deployment of the artifacts
- 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 NON-SNAPSHOT VERSIONS)
- Closes the milestone on Github (e.g. `v1.0.1.RELEASE`) (ONLY FOR NON-SNAPSHOT VERSIONS)
=== How to run it
@@ -57,6 +58,7 @@ The application will start running from your working directory.
- `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`
- `releaser.git.oauth-token` - GitHub OAuth token to be used to interact with GitHub repo.
- `releaser.maven.build-command` - Command to be executed to build the project. Defaults to `./mvnw clean install -Pdocs`
- `releaser.maven.deploy-command` - Command to be executed to deploy a built project". Defaults to `./mvnw deploy -DskipTests -Pfast`
- `releaser.maven.publish-docs-commands` - Command to be executed to deploy a built project. If present `{{version}}` will be replaced by the proper version.

View File

@@ -25,6 +25,7 @@ why this tool makes it easy to automate the release / dependency update process
- Runs the deployment of the artifacts
- 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 NON-SNAPSHOT VERSIONS)
- Closes the milestone on Github (e.g. `v1.0.1.RELEASE`) (ONLY FOR NON-SNAPSHOT VERSIONS)
=== How to run it
@@ -47,6 +48,7 @@ The application will start running from your working directory.
- `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`
- `releaser.git.oauth-token` - GitHub OAuth token to be used to interact with GitHub repo.
- `releaser.maven.build-command` - Command to be executed to build the project. Defaults to `./mvnw clean install -Pdocs`
- `releaser.maven.deploy-command` - Command to be executed to deploy a built project". Defaults to `./mvnw deploy -DskipTests -Pfast`
- `releaser.maven.publish-docs-commands` - Command to be executed to deploy a built project. If present `{{version}}` will be replaced by the proper version.

View File

@@ -68,6 +68,11 @@
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.jcabi</groupId>
<artifactId>jcabi-github</artifactId>
<version>0.23</version>
</dependency>
</dependencies>
<profiles>

View File

@@ -65,4 +65,9 @@ public class Releaser {
this.projectGitUpdater.pushCurrentBranch(project);
log.info("\nSuccessfully pushed current branch");
}
public void closeMilestone(ProjectVersion releaseVersion) {
this.projectGitUpdater.closeMilestone(releaseVersion);
log.info("\nSuccessfully closed milestone");
}
}

View File

@@ -52,6 +52,11 @@ public class ReleaserProperties {
*/
private String cloneDestinationDir;
/**
* GitHub OAuth token to be used to interact with GitHub repo
*/
private String oauthToken;
public String getSpringCloudReleaseGitUrl() {
return this.springCloudReleaseGitUrl;
}
@@ -68,6 +73,13 @@ public class ReleaserProperties {
this.cloneDestinationDir = cloneDestinationDir;
}
public String getOauthToken() {
return this.oauthToken;
}
public void setOauthToken(String oauthToken) {
this.oauthToken = oauthToken;
}
}
public static class Pom {

View File

@@ -0,0 +1,86 @@
package org.springframework.cloud.release.internal.git;
import java.io.IOException;
import java.lang.invoke.MethodHandles;
import java.util.HashMap;
import java.util.Map;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.cloud.release.internal.ReleaserProperties;
import org.springframework.cloud.release.internal.pom.ProjectVersion;
import org.springframework.util.Assert;
import com.jcabi.github.Coordinates;
import com.jcabi.github.Github;
import com.jcabi.github.Milestone;
import com.jcabi.github.RtGithub;
import com.jcabi.http.wire.RetryWire;
/**
* @author Marcin Grzejszczak
*/
class MilestoneCloser {
private static final Logger log = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass());
private final Github github;
private final ReleaserProperties properties;
MilestoneCloser(ReleaserProperties properties) {
this.github = new RtGithub(new RtGithub(
properties.getGit().getOauthToken()).entry().through(RetryWire.class));
this.properties = properties;
}
MilestoneCloser(Github github, ReleaserProperties properties) {
this.github = github;
this.properties = properties;
}
void closeMilestone(ProjectVersion version) {
Assert.hasText(this.properties.getGit().getOauthToken(),
"You have to pass Github OAuth token for milestone closing to be operational");
Iterable<Milestone> milestones = milestones(version);
log.info("Successfully received list of milestones");
boolean matchingMilestone = false;
for (Milestone milestone : milestones) {
Milestone.Smart smartMilestone = new Milestone.Smart(milestone);
try {
String tagVersion = "v" + version.version;
if (tagVersion.equals(milestoneTitle(smartMilestone))) {
log.info("Found a matching milestone - closing it");
smartMilestone.close();
matchingMilestone = true;
log.info("Closed the [{}] milestone", tagVersion);
}
}
catch (IOException e) {
throw new IllegalStateException(e);
}
}
if (!matchingMilestone) {
throw new IllegalStateException("No matching milestone was found");
}
}
String milestoneTitle(Milestone.Smart milestone) throws IOException {
return milestone.title();
}
private Iterable<Milestone> milestones(ProjectVersion version) {
return this.github.repos()
.get(new Coordinates.Simple(org(), version.projectName))
.milestones().iterate(openMilestones());
}
String org() {
return "spring-cloud";
}
private Map<String, String> openMilestones() {
Map<String, String> params = new HashMap<>();
params.put("state", "open");
return params;
}
}

View File

@@ -25,9 +25,11 @@ public class ProjectGitUpdater {
private static final String POST_RELEASE_BUMP_MSG = "Bumping versions to %s after release";
private final ReleaserProperties properties;
private final MilestoneCloser milestoneCloser;
public ProjectGitUpdater(ReleaserProperties properties) {
this.properties = properties;
this.milestoneCloser = new MilestoneCloser(properties);
}
public void commitAndTagIfApplicable(File project, ProjectVersion version) {
@@ -84,6 +86,10 @@ public class ProjectGitUpdater {
gitRepo(project).pushCurrentBranch(project);
}
public void closeMilestone(ProjectVersion releaseVersion) {
this.milestoneCloser.closeMilestone(releaseVersion);
}
GitRepo gitRepo(File workingDir) {
return new GitRepo(workingDir);
}

View File

@@ -2,6 +2,8 @@ package org.springframework.cloud.release.internal.pom;
import java.io.File;
import org.apache.maven.model.Model;
/**
* Object representing a root project's version.
* Knows how to provide a minor bumped version;
@@ -10,15 +12,27 @@ import java.io.File;
*/
public class ProjectVersion {
public final String projectName;
public final String version;
private final PomReader pomReader = new PomReader();
public ProjectVersion(String version) {
public ProjectVersion(String projectName, String version) {
this.projectName = nameWithoutParent(projectName);
this.version = version;
}
public ProjectVersion(File project) {
this.version = this.pomReader.readPom(project).getVersion();
Model model = this.pomReader.readPom(project);
this.projectName = nameWithoutParent(model.getArtifactId());
this.version = model.getVersion();
}
private String nameWithoutParent(String projectName) {
boolean containsParent = projectName.endsWith("-parent");
if (!containsParent) {
return projectName;
}
return projectName.substring(0, projectName.indexOf("-parent"));
}
public String bumpedVersion() {

View File

@@ -0,0 +1,114 @@
package org.springframework.cloud.release.internal.git;
import java.io.IOException;
import java.net.URISyntaxException;
import javax.json.Json;
import org.junit.Before;
import org.junit.Test;
import org.springframework.cloud.release.internal.ReleaserProperties;
import org.springframework.cloud.release.internal.pom.ProjectVersion;
import com.jcabi.github.Milestone;
import com.jcabi.github.Repo;
import com.jcabi.github.mock.MkGithub;
import static org.assertj.core.api.BDDAssertions.thenThrownBy;
/**
* @author Marcin Grzejszczak
*/
public class MilestoneCloserTests {
MkGithub github;
Repo repo;
@Before
public void setup() throws URISyntaxException, IOException {
this.github = new MkGithub();
this.repo = createSleuthRepo(this.github);
}
@Test
public void should_close_milestone_if_there_is_one() throws IOException {
MilestoneCloser closer = new MilestoneCloser(this.github, withToken()) {
@Override String org() {
return repo.coordinates().user();
}
@Override String milestoneTitle(Milestone.Smart milestone)
throws IOException {
return "v0.2.0.BUILD-SNAPSHOT";
}
};
repo.milestones().create("v0.2.0.BUILD-SNAPSHOT");
closer.closeMilestone(sleuthProject());
}
private ProjectVersion sleuthProject() {
return new ProjectVersion("spring-cloud-sleuth", "0.2.0.BUILD-SNAPSHOT");
}
@Test
public void should_throw_exception_when_there_is_no_matching_milestone() throws IOException {
MilestoneCloser closer = new MilestoneCloser(this.github, withToken()) {
@Override String org() {
return repo.coordinates().user();
}
@Override String milestoneTitle(Milestone.Smart milestone)
throws IOException {
return "0.1.0.BUILD-SNAPSHOT";
}
};
repo.milestones().create("v0.2.0.BUILD-SNAPSHOT");
thenThrownBy(() -> closer.closeMilestone(sleuthProject()))
.isInstanceOf(IllegalStateException.class)
.hasMessageContaining("No matching milestone was found");
}
@Test
public void should_throw_exception_when_io_problems_occurred() throws IOException {
MilestoneCloser closer = new MilestoneCloser(this.github, withToken()) {
@Override String org() {
return repo.coordinates().user();
}
@Override String milestoneTitle(Milestone.Smart milestone)
throws IOException {
throw new IOException("foo");
}
};
repo.milestones().create("v0.2.0.BUILD-SNAPSHOT");
thenThrownBy(() -> closer.closeMilestone(sleuthProject()))
.isInstanceOf(IllegalStateException.class)
.hasMessageContaining("foo");
}
private Repo createSleuthRepo(MkGithub github) throws IOException {
return github.repos().create(
Json.createObjectBuilder().add(
"name",
"spring-cloud-sleuth"
).build()
);
}
@Test
public void should_throw_exception_when_no_token_was_passed() {
MilestoneCloser closer = new MilestoneCloser(new ReleaserProperties());
thenThrownBy(() -> closer.closeMilestone(sleuthProject()))
.isInstanceOf(IllegalArgumentException.class)
.hasMessageContaining("You have to pass Github OAuth token for milestone closing to be operational");
}
ReleaserProperties withToken() {
ReleaserProperties properties = new ReleaserProperties();
properties.getGit().setOauthToken("foo");
return properties;
}
}

View File

@@ -31,7 +31,7 @@ public class ProjectGitUpdaterTests {
@Test
public void should_only_commit_without_pushing_changes_when_version_is_snapshot() {
this.updater.commitAndTagIfApplicable(this.file, new ProjectVersion("1.0.0.BUILD-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());
@@ -39,7 +39,7 @@ public class ProjectGitUpdaterTests {
@Test
public void should_commit_tag_and_push_tag_when_version_is_not_snapshot() {
this.updater.commitAndTagIfApplicable(this.file, new ProjectVersion("1.0.0.RELEASE"));
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"));
@@ -48,7 +48,7 @@ public class ProjectGitUpdaterTests {
@Test
public void should_commit_when_snapshot_version_is_present_with_post_release_msg() {
this.updater.commitAfterBumpingVersions(this.file, new ProjectVersion("1.0.0.BUILD-SNAPSHOT"));
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());
@@ -56,7 +56,7 @@ public class ProjectGitUpdaterTests {
@Test
public void should_not_commit_when_non_snapshot_version_is_present() {
this.updater.commitAfterBumpingVersions(this.file, new ProjectVersion("1.0.0.RELEASE"));
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());
@@ -64,14 +64,14 @@ public class ProjectGitUpdaterTests {
@Test
public void should_not_revert_changes_for_snapshots() {
this.updater.revertChangesIfApplicable(this.file, new ProjectVersion("1.0.0.BUILD-SNAPSHOT"));
this.updater.revertChangesIfApplicable(this.file, projectVersion("1.0.0.BUILD-SNAPSHOT"));
then(this.gitRepo).should(never()).revert(any(File.class), anyString());
}
@Test
public void should_revert_changes_when_version_is_not_snapshot() {
this.updater.revertChangesIfApplicable(this.file, new ProjectVersion("1.0.0.RELEASE"));
this.updater.revertChangesIfApplicable(this.file, projectVersion("1.0.0.RELEASE"));
then(this.gitRepo).should().revert(any(File.class), eq("Going back to snapshots"));
}
@@ -82,4 +82,8 @@ public class ProjectGitUpdaterTests {
then(this.gitRepo).should().pushCurrentBranch(any(File.class));
}
private ProjectVersion projectVersion(String version) {
return new ProjectVersion("foo", version);
}
}

View File

@@ -17,11 +17,30 @@ import static org.assertj.core.api.BDDAssertions.thenThrownBy;
public class ProjectVersionTests {
File springCloudReleaseProject;
File springCloudContract;
@Before
public void setup() throws URISyntaxException {
URI scRelease = GitRepoTests.class.getResource("/projects/spring-cloud-release").toURI();
URI scContract = GitRepoTests.class.getResource("/projects/spring-cloud-contract").toURI();
this.springCloudReleaseProject = new File(scRelease.getPath(), "pom.xml");
this.springCloudContract = new File(scContract.getPath(), "pom.xml");
}
@Test
public void should_build_version_from_text_when_parent_suffix_is_present() {
ProjectVersion projectVersion = new ProjectVersion("foo-parent", "1.0.0");
then(projectVersion.version).isEqualTo("1.0.0");
then(projectVersion.projectName).isEqualTo("foo");
}
@Test
public void should_build_version_from_text() {
ProjectVersion projectVersion = new ProjectVersion("foo", "1.0.0");
then(projectVersion.version).isEqualTo("1.0.0");
then(projectVersion.projectName).isEqualTo("foo");
}
@Test
@@ -29,13 +48,22 @@ public class ProjectVersionTests {
ProjectVersion projectVersion = new ProjectVersion(this.springCloudReleaseProject);
then(projectVersion.version).isEqualTo("Dalston.BUILD-SNAPSHOT");
then(projectVersion.projectName).isEqualTo("spring-cloud-starter-build");
}
@Test
public void should_build_version_from_file_when_parent_suffix_is_present() {
ProjectVersion projectVersion = new ProjectVersion(this.springCloudContract);
then(projectVersion.version).isEqualTo("1.1.0.BUILD-SNAPSHOT");
then(projectVersion.projectName).isEqualTo("spring-cloud-contract");
}
@Test
public void should_throw_exception_if_version_is_not_long_enough() {
String version = "1.0";
thenThrownBy(() -> new ProjectVersion(version).bumpedVersion())
thenThrownBy(() -> projectVersion(version).bumpedVersion())
.isInstanceOf(IllegalStateException.class)
.hasMessageContaining("Version is invalid");
}
@@ -44,21 +72,24 @@ public class ProjectVersionTests {
public void should_bump_version_by_patch_version() {
String version = "1.0.1.BUILD-SNAPSHOT";
then(new ProjectVersion(version).bumpedVersion()).isEqualTo("1.0.2.BUILD-SNAPSHOT");
then(projectVersion(version).bumpedVersion()).isEqualTo("1.0.2.BUILD-SNAPSHOT");
}
@Test
public void should_return_true_for_snapshot_version() {
String version = "1.0.1.BUILD-SNAPSHOT";
then(new ProjectVersion(version).isSnapshot()).isTrue();
then(projectVersion(version).isSnapshot()).isTrue();
}
@Test
public void should_return_false_for_snapshot_version() {
String version = "1.0.1.RELEASE";
then(new ProjectVersion(version).isSnapshot()).isFalse();
then(projectVersion(version).isSnapshot()).isFalse();
}
private ProjectVersion projectVersion(String version) {
return new ProjectVersion("foo", version);
}
}

View File

@@ -61,7 +61,8 @@ public class SpringReleaser {
this.releaser.publishDocs(changedVersion);
}
if (!changedVersion.isSnapshot()) {
log.info("\n\n\n=== REVERTING CHANGES & BUMPING VERSION===\n\nPress ENTER to go back to snapshots and bump originalVersion by patch {}", MSG);
log.info("\n\n\n=== REVERTING CHANGES & BUMPING VERSION===\n\nPress ENTER to go "
+ "back to snapshots and bump originalVersion by patch {}", MSG);
boolean skipRevert = skipStep();
if (!skipRevert) {
this.releaser.rollbackReleaseVersion(project, originalVersion, changedVersion);
@@ -72,6 +73,13 @@ public class SpringReleaser {
if (!skipPush) {
this.releaser.pushCurrentBranch(project);
}
if (!changedVersion.isSnapshot()) {
log.info("\n\n\n=== CLOSING MILESTONE===\n\nPress ENTER to close the milestone at Github {}", MSG);
boolean skipMilestone = skipStep();
if (!skipMilestone) {
this.releaser.closeMilestone(changedVersion);
}
}
}
boolean skipStep() {

View File

@@ -17,6 +17,7 @@ import org.springframework.cloud.release.internal.ReleaserProperties;
import org.springframework.cloud.release.internal.git.GitTestUtils;
import org.springframework.cloud.release.internal.git.ProjectGitUpdater;
import org.springframework.cloud.release.internal.pom.ProjectPomUpdater;
import org.springframework.cloud.release.internal.pom.ProjectVersion;
import org.springframework.cloud.release.internal.pom.TestPomReader;
import org.springframework.cloud.release.internal.pom.TestUtils;
import org.springframework.cloud.release.internal.project.ProjectBuilder;
@@ -33,6 +34,7 @@ public class AcceptanceTests {
TestPomReader testPomReader = new TestPomReader();
File springCloudConsulProject;
File temporaryFolder;
TestProjectGitUpdater gitUpdater;
@Before
public void setup() throws Exception {
@@ -61,6 +63,7 @@ public class AcceptanceTests {
commitIsPresent(iterator, "Update SNAPSHOT to 1.1.2.RELEASE");
pomVersionIsEqualTo(project, "1.2.1.BUILD-SNAPSHOT");
pomParentVersionIsEqualTo(project, "1.2.1.BUILD-SNAPSHOT");
then(this.gitUpdater.executed).isTrue();
}
private Iterable<RevCommit> listOfCommits(File project) throws GitAPIException {
@@ -106,7 +109,8 @@ public class AcceptanceTests {
ReleaserProperties properties = releaserProperties(projectFile);
ProjectPomUpdater pomUpdater = new ProjectPomUpdater(properties);
ProjectBuilder projectBuilder = new ProjectBuilder(properties, pomUpdater);
ProjectGitUpdater gitUpdater = new ProjectGitUpdater(properties);
TestProjectGitUpdater gitUpdater = new TestProjectGitUpdater(properties);
this.gitUpdater = gitUpdater;
return new SpringReleaser(new Releaser(pomUpdater, projectBuilder, gitUpdater), properties) {
@Override boolean skipStep() {
return false;
@@ -114,6 +118,21 @@ public class AcceptanceTests {
};
}
class TestProjectGitUpdater extends ProjectGitUpdater {
boolean executed = false;
public TestProjectGitUpdater(ReleaserProperties properties) {
super(properties);
}
@Override public void closeMilestone(ProjectVersion releaseVersion) {
then(releaseVersion.projectName).isEqualTo("spring-cloud-consul");
then(releaseVersion.version).isEqualTo("1.1.2.RELEASE");
this.executed = true;
}
}
private File tmpFile(String relativePath) {
return new File(this.temporaryFolder, relativePath);
}