فهرست منبع

Add workflow that triggers release on due date

Add 2 Gradle tasks, one that calculates the next release milestone based on the current version and one that checks if it is due today.

Issue gh-10451
Issue gh-10455
Eleftheria Stein 3 سال پیش
والد
کامیت
1a1ef42094

+ 91 - 0
.github/workflows/release-next-version.yml

@@ -0,0 +1,91 @@
+name: Release Next Version
+
+on:
+  push:
+  schedule:
+    - cron: '0 0 * * MON' # Every Monday
+  workflow_dispatch: # Manual trigger
+
+env:
+  SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }}
+  GRADLE_ENTERPRISE_CACHE_USER: ${{ secrets.GRADLE_ENTERPRISE_CACHE_USER }}
+  GRADLE_ENTERPRISE_CACHE_PASSWORD: ${{ secrets.GRADLE_ENTERPRISE_CACHE_PASSWORD }}
+  GRADLE_ENTERPRISE_SECRET_ACCESS_KEY: ${{ secrets.GRADLE_ENTERPRISE_SECRET_ACCESS_KEY }}
+  RUN_JOBS: ${{ github.repository == 'spring-projects/spring-security' }}
+
+jobs:
+  prerequisites:
+    name: Pre-requisites for building
+    runs-on: ubuntu-latest
+    outputs:
+      runjobs: ${{ steps.continue.outputs.runjobs }}
+    steps:
+      - id: continue
+        name: Determine if should continue
+        if: env.RUN_JOBS == 'true'
+        run: echo "::set-output name=runjobs::true"
+  check_release_due:
+    name: Check if the release is due today
+    needs: [prerequisites]
+    runs-on: ubuntu-latest
+    if: needs.prerequisites.outputs.runjobs
+    steps:
+      - uses: actions/checkout@v2
+      - name: Set up JDK 17
+        uses: actions/setup-java@v1
+        with:
+          java-version: '17'
+      - name: Setup gradle user name
+        run: |
+          mkdir -p ~/.gradle
+          echo 'systemProp.user.name=spring-builds+github' >> ~/.gradle/gradle.properties
+      - name: Cache Gradle packages
+        uses: actions/cache@v2
+        with:
+          path: ~/.gradle/caches
+          key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle') }}
+      - name: Check release
+        env:
+          GRADLE_ENTERPRISE_CACHE_USERNAME: ${{ secrets.GRADLE_ENTERPRISE_CACHE_USER }}
+          GRADLE_ENTERPRISE_CACHE_PASSWORD: ${{ secrets.GRADLE_ENTERPRISE_CACHE_PASSWORD }}
+          GRADLE_ENTERPRISE_ACCESS_KEY: ${{ secrets.GRADLE_ENTERPRISE_SECRET_ACCESS_KEY }}
+        run: ./gradlew gitHubCheckNextVersionDueToday
+  release:
+    name: Release next version
+    needs: [check_release_due]
+    runs-on: ubuntu-latest
+    steps:
+      - uses: actions/checkout@v2
+      - name: Set up JDK
+        uses: actions/setup-java@v1
+        with:
+          java-version: '17'
+      - name: Setup gradle user name
+        run: |
+          mkdir -p ~/.gradle
+          echo 'systemProp.user.name=spring-builds+github' >> ~/.gradle/gradle.properties
+      - name: Deploy artifacts
+        run: |
+          export GRADLE_ENTERPRISE_CACHE_USERNAME="$GRADLE_ENTERPRISE_CACHE_USER"
+          export GRADLE_ENTERPRISE_CACHE_PASSWORD="$GRADLE_ENTERPRISE_CACHE_PASSWORD"
+          export GRADLE_ENTERPRISE_ACCESS_KEY="$GRADLE_ENTERPRISE_SECRET_ACCESS_KEY"
+          echo "Release task: use input from gitHubNextReleaseMilestone task"
+          ./gradlew gitHubNextReleaseMilestone
+        env:
+          ORG_GRADLE_PROJECT_signingKey: ${{ secrets.GPG_PRIVATE_KEY }}
+          ORG_GRADLE_PROJECT_signingPassword: ${{ secrets.GPG_PASSPHRASE }}
+          OSSRH_TOKEN_USERNAME: ${{ secrets.OSSRH_S01_TOKEN_USERNAME }}
+          OSSRH_TOKEN_PASSWORD: ${{ secrets.OSSRH_S01_TOKEN_PASSWORD }}
+  notify_result:
+    name: Check for failures
+    needs: [release]
+    if: failure()
+    runs-on: ubuntu-latest
+    steps:
+      - name: Send Slack message
+        uses: Gamesight/slack-workflow-status@v1.0.1
+        with:
+          repo_token: ${{ secrets.GITHUB_TOKEN }}
+          slack_webhook_url: ${{ secrets.SLACK_WEBHOOK_URL }}
+          channel: '#spring-security-ci'
+          name: 'CI Notifier'

+ 14 - 0
build.gradle

@@ -47,6 +47,20 @@ tasks.named("gitHubCheckMilestoneHasNoOpenIssues") {
 	}
 }
 
+tasks.named("gitHubNextReleaseMilestone") {
+	repository {
+		owner = "spring-projects"
+		name = "spring-security"
+	}
+}
+
+tasks.named("gitHubCheckNextVersionDueToday") {
+	repository {
+		owner = "spring-projects"
+		name = "spring-security"
+	}
+}
+
 tasks.named("createGitHubRelease") {
 	repository {
 		owner = "spring-projects"

+ 127 - 1
buildSrc/src/main/java/org/springframework/gradle/github/milestones/GitHubMilestoneApi.java

@@ -1,5 +1,5 @@
 /*
- * Copyright 2019-2020 the original author or authors.
+ * Copyright 2019-2022 the original author or authors.
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
@@ -17,7 +17,11 @@
 package org.springframework.gradle.github.milestones;
 
 import java.io.IOException;
+import java.time.Instant;
 import java.util.List;
+import java.util.Optional;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
 
 import com.google.common.reflect.TypeToken;
 import com.google.gson.Gson;
@@ -89,6 +93,128 @@ public class GitHubMilestoneApi {
 		}
 	}
 
+	/**
+	 * Check if the given milestone is due today or past due.
+	 *
+	 * @param repositoryRef The repository owner/name
+	 * @param milestoneTitle The title of the milestone whose due date should be checked
+	 * @return true if the given milestone is due today or past due, false otherwise
+	 */
+	public boolean isMilestoneDueToday(RepositoryRef repositoryRef, String milestoneTitle) {
+		String url = this.baseUrl + "/repos/" + repositoryRef.getOwner() + "/" + repositoryRef.getName()
+				+ "/milestones?per_page=100";
+		Request request = new Request.Builder().get().url(url).build();
+		try {
+			Response response = this.client.newCall(request).execute();
+			if (!response.isSuccessful()) {
+				throw new RuntimeException("Could not find milestone with title " + milestoneTitle + " for repository "
+						+ repositoryRef + ". Response " + response);
+			}
+			List<Milestone> milestones = this.gson.fromJson(response.body().charStream(),
+					new TypeToken<List<Milestone>>() {
+					}.getType());
+			for (Milestone milestone : milestones) {
+				if (milestoneTitle.equals(milestone.getTitle())) {
+					Instant now = Instant.now();
+					return milestone.getDueOn() != null && now.isAfter(milestone.getDueOn().toInstant());
+				}
+			}
+			if (milestones.size() <= 100) {
+				throw new RuntimeException("Could not find open milestone with title " + milestoneTitle
+						+ " for repository " + repositoryRef + " Got " + milestones);
+			}
+			throw new RuntimeException(
+					"It is possible there are too many open milestones open (only 100 are supported). Could not find open milestone with title "
+							+ milestoneTitle + " for repository " + repositoryRef + " Got " + milestones);
+		}
+		catch (IOException e) {
+			throw new RuntimeException(
+					"Could not find open milestone with title " + milestoneTitle + " for repository " + repositoryRef,
+					e);
+		}
+	}
+
+	/**
+	 * Calculate the next release version based on the current version.
+	 *
+	 * The current version must conform to the pattern MAJOR.MINOR.PATCH-SNAPSHOT. If the
+	 * current version is a snapshot of a patch release, then the patch release will be
+	 * returned. For example, if the current version is 5.6.1-SNAPSHOT, then 5.6.1 will be
+	 * returned. If the current version is a snapshot of a version that is not GA (i.e the
+	 * PATCH segment is 0), then GitHub will be queried to find the next milestone or
+	 * release candidate. If no pre-release versions are found, then the next version will
+	 * be assumed to be the GA.
+	 * @param repositoryRef The repository owner/name
+	 * @param currentVersion The current project version
+	 * @return the next matching milestone/release candidate or null if none exist
+	 */
+	public String getNextReleaseMilestone(RepositoryRef repositoryRef, String currentVersion) {
+		Pattern snapshotPattern = Pattern.compile("^([0-9]+)\\.([0-9]+)\\.([0-9]+)-SNAPSHOT$");
+		Matcher snapshotVersion = snapshotPattern.matcher(currentVersion);
+
+		if (snapshotVersion.find()) {
+			String patchSegment = snapshotVersion.group(3);
+			String currentVersionNoIdentifier = currentVersion.replace("-SNAPSHOT", "");
+			if (patchSegment.equals("0")) {
+				String nextPreRelease = getNextPreRelease(repositoryRef, currentVersionNoIdentifier);
+				return nextPreRelease != null ? nextPreRelease : currentVersionNoIdentifier;
+			}
+			else {
+				return currentVersionNoIdentifier;
+			}
+		}
+		else {
+			throw new IllegalStateException(
+					"Cannot calculate next release version because the current project version does not conform to the expected format");
+		}
+	}
+
+	/**
+	 * Calculate the next pre-release version (milestone or release candidate) based on
+	 * the current version.
+	 *
+	 * The current version must conform to the pattern MAJOR.MINOR.PATCH. If no matching
+	 * milestone or release candidate is found in GitHub then it will return null.
+	 * @param repositoryRef The repository owner/name
+	 * @param currentVersionNoIdentifier The current project version without any
+	 * identifier
+	 * @return the next matching milestone/release candidate or null if none exist
+	 */
+	private String getNextPreRelease(RepositoryRef repositoryRef, String currentVersionNoIdentifier) {
+		String url = this.baseUrl + "/repos/" + repositoryRef.getOwner() + "/" + repositoryRef.getName()
+				+ "/milestones?per_page=100";
+		Request request = new Request.Builder().get().url(url).build();
+		try {
+			Response response = this.client.newCall(request).execute();
+			if (!response.isSuccessful()) {
+				throw new RuntimeException(
+						"Could not get milestones for repository " + repositoryRef + ". Response " + response);
+			}
+			List<Milestone> milestones = this.gson.fromJson(response.body().charStream(),
+					new TypeToken<List<Milestone>>() {
+					}.getType());
+			Optional<String> nextPreRelease = milestones.stream().map(Milestone::getTitle)
+					.filter(m -> m.startsWith(currentVersionNoIdentifier + "-"))
+					.min((m1, m2) -> {
+						Pattern preReleasePattern = Pattern.compile("^.*-([A-Z]+)([0-9]+)$");
+						Matcher matcher1 = preReleasePattern.matcher(m1);
+						Matcher matcher2 = preReleasePattern.matcher(m2);
+						matcher1.find();
+						matcher2.find();
+						if (!matcher1.group(1).equals(matcher2.group(1))) {
+							return m1.compareTo(m2);
+						}
+						else {
+							return Integer.valueOf(matcher1.group(2)).compareTo(Integer.valueOf(matcher2.group(2)));
+						}
+					});
+			return nextPreRelease.orElse(null);
+		}
+		catch (IOException e) {
+			throw new RuntimeException("Could not find open milestones with for repository " + repositoryRef, e);
+		}
+	}
+
 //	public boolean isOpenIssuesForMilestoneName(String owner, String repository, String milestoneName) {
 //
 //	}

+ 93 - 0
buildSrc/src/main/java/org/springframework/gradle/github/milestones/GitHubMilestoneNextReleaseTask.java

@@ -0,0 +1,93 @@
+/*
+ * Copyright 2019-2022 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.springframework.gradle.github.milestones;
+
+import org.gradle.api.Action;
+import org.gradle.api.DefaultTask;
+import org.gradle.api.file.RegularFileProperty;
+import org.gradle.api.tasks.Input;
+import org.gradle.api.tasks.Optional;
+import org.gradle.api.tasks.OutputFile;
+import org.gradle.api.tasks.TaskAction;
+import org.yaml.snakeyaml.DumperOptions;
+import org.yaml.snakeyaml.Yaml;
+import org.yaml.snakeyaml.nodes.Tag;
+import org.yaml.snakeyaml.representer.Representer;
+
+import java.io.File;
+import java.io.FileWriter;
+import java.io.IOException;
+
+import org.springframework.gradle.github.RepositoryRef;
+
+public abstract class GitHubMilestoneNextReleaseTask extends DefaultTask {
+
+	@Input
+	private RepositoryRef repository = new RepositoryRef();
+
+	@Input
+	@Optional
+	private String gitHubAccessToken;
+
+	private GitHubMilestoneApi milestones = new GitHubMilestoneApi();
+
+	@TaskAction
+	public void calculateNextReleaseMilestone() throws IOException {
+		String currentVersion = getProject().getVersion().toString();
+		String nextPreRelease = this.milestones.getNextReleaseMilestone(this.repository, currentVersion);
+		System.out.println("The next release milestone is: " + nextPreRelease);
+		NextVersionYml nextVersionYml = new NextVersionYml();
+		nextVersionYml.setVersion(nextPreRelease);
+		File outputFile = getNextReleaseFile().get().getAsFile();
+		FileWriter outputWriter = new FileWriter(outputFile);
+		Yaml yaml = getYaml();
+		yaml.dump(nextVersionYml, outputWriter);
+	}
+
+	@OutputFile
+	public abstract RegularFileProperty getNextReleaseFile();
+
+	public RepositoryRef getRepository() {
+		return repository;
+	}
+
+	public void repository(Action<RepositoryRef> repository) {
+		repository.execute(this.repository);
+	}
+
+	public void setRepository(RepositoryRef repository) {
+		this.repository = repository;
+	}
+
+	public String getGitHubAccessToken() {
+		return gitHubAccessToken;
+	}
+
+	public void setGitHubAccessToken(String gitHubAccessToken) {
+		this.gitHubAccessToken = gitHubAccessToken;
+		this.milestones = new GitHubMilestoneApi(gitHubAccessToken);
+	}
+
+	private Yaml getYaml() {
+		Representer representer = new Representer();
+		representer.addClassTag(NextVersionYml.class, Tag.MAP);
+		DumperOptions ymlOptions = new DumperOptions();
+		ymlOptions.setDefaultFlowStyle(DumperOptions.FlowStyle.BLOCK);
+		return new Yaml(representer, ymlOptions);
+	}
+
+}

+ 89 - 0
buildSrc/src/main/java/org/springframework/gradle/github/milestones/GitHubMilestoneNextVersionDueTodayTask.java

@@ -0,0 +1,89 @@
+/*
+ * Copyright 2019-2022 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.springframework.gradle.github.milestones;
+
+import org.gradle.api.Action;
+import org.gradle.api.DefaultTask;
+import org.gradle.api.file.RegularFileProperty;
+import org.gradle.api.tasks.Input;
+import org.gradle.api.tasks.InputFile;
+import org.gradle.api.tasks.Optional;
+import org.gradle.api.tasks.TaskAction;
+import org.yaml.snakeyaml.Yaml;
+import org.yaml.snakeyaml.constructor.Constructor;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileNotFoundException;
+
+import org.springframework.gradle.github.RepositoryRef;
+
+public abstract class GitHubMilestoneNextVersionDueTodayTask extends DefaultTask {
+
+	@Input
+	private RepositoryRef repository = new RepositoryRef();
+
+	@Input
+	@Optional
+	private String gitHubAccessToken;
+
+	@InputFile
+	public abstract RegularFileProperty getNextVersionFile();
+
+	private GitHubMilestoneApi milestones = new GitHubMilestoneApi();
+
+	@TaskAction
+	public void checkReleaseDueToday() throws FileNotFoundException {
+		File nextVersionFile = getNextVersionFile().getAsFile().get();
+		Yaml yaml = new Yaml(new Constructor(NextVersionYml.class));
+		NextVersionYml nextVersionYml = yaml.load(new FileInputStream(nextVersionFile));
+		String nextVersion = nextVersionYml.getVersion();
+		if (nextVersion == null) {
+			throw new IllegalArgumentException(
+					"Could not find version property in provided file " + nextVersionFile.getName());
+		}
+		boolean milestoneDueToday = this.milestones.isMilestoneDueToday(this.repository, nextVersion);
+		if (!milestoneDueToday) {
+			throw new IllegalStateException("The milestone with the title " + nextVersion + " in the repository "
+					+ this.repository + " is not due yet");
+		}
+		System.out.println("The milestone with the title " + nextVersion + " in the repository " + this.repository
+				+ " is due today");
+	}
+
+	public RepositoryRef getRepository() {
+		return repository;
+	}
+
+	public void repository(Action<RepositoryRef> repository) {
+		repository.execute(this.repository);
+	}
+
+	public void setRepository(RepositoryRef repository) {
+		this.repository = repository;
+	}
+
+	public String getGitHubAccessToken() {
+		return gitHubAccessToken;
+	}
+
+	public void setGitHubAccessToken(String gitHubAccessToken) {
+		this.gitHubAccessToken = gitHubAccessToken;
+		this.milestones = new GitHubMilestoneApi(gitHubAccessToken);
+	}
+
+}

+ 29 - 1
buildSrc/src/main/java/org/springframework/gradle/github/milestones/GitHubMilestonePlugin.java

@@ -1,5 +1,5 @@
 /*
- * Copyright 2019-2020 the original author or authors.
+ * Copyright 2019-2022 the original author or authors.
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
@@ -19,6 +19,7 @@ package org.springframework.gradle.github.milestones;
 import org.gradle.api.Action;
 import org.gradle.api.Plugin;
 import org.gradle.api.Project;
+import org.gradle.api.tasks.TaskProvider;
 
 public class GitHubMilestonePlugin implements Plugin<Project> {
 	@Override
@@ -34,5 +35,32 @@ public class GitHubMilestonePlugin implements Plugin<Project> {
 				}
 			}
 		});
+		TaskProvider<GitHubMilestoneNextReleaseTask> nextReleaseMilestoneTask = project.getTasks().register("gitHubNextReleaseMilestone", GitHubMilestoneNextReleaseTask.class, new Action<GitHubMilestoneNextReleaseTask>() {
+					@Override
+					public void execute(GitHubMilestoneNextReleaseTask gitHubMilestoneNextReleaseTask) {
+						gitHubMilestoneNextReleaseTask.doNotTrackState("API call to GitHub needs to check for new milestones every time");
+						gitHubMilestoneNextReleaseTask.setGroup("Release");
+						gitHubMilestoneNextReleaseTask.setDescription("Calculates the next release version based on the current version and outputs it to a yaml file");
+						gitHubMilestoneNextReleaseTask.getNextReleaseFile()
+								.fileProvider(project.provider(() -> project.file("next-release.yml")));
+						if (project.hasProperty("gitHubAccessToken")) {
+							gitHubMilestoneNextReleaseTask
+									.setGitHubAccessToken((String) project.findProperty("gitHubAccessToken"));
+						}
+					}
+				});
+		project.getTasks().register("gitHubCheckNextVersionDueToday", GitHubMilestoneNextVersionDueTodayTask.class, new Action<GitHubMilestoneNextVersionDueTodayTask>() {
+					@Override
+					public void execute(GitHubMilestoneNextVersionDueTodayTask gitHubMilestoneNextVersionDueTodayTask) {
+						gitHubMilestoneNextVersionDueTodayTask.setGroup("Release");
+						gitHubMilestoneNextVersionDueTodayTask.setDescription("Checks if the next release version is due today or past due, will fail if the next version is not due yet");
+						gitHubMilestoneNextVersionDueTodayTask.getNextVersionFile().convention(
+								nextReleaseMilestoneTask.flatMap(GitHubMilestoneNextReleaseTask::getNextReleaseFile));
+						if (project.hasProperty("gitHubAccessToken")) {
+							gitHubMilestoneNextVersionDueTodayTask
+									.setGitHubAccessToken((String) project.findProperty("gitHubAccessToken"));
+						}
+					}
+				});
 	}
 }

+ 33 - 1
buildSrc/src/main/java/org/springframework/gradle/github/milestones/Milestone.java

@@ -1,10 +1,33 @@
+/*
+ * Copyright 2019-2022 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
 package org.springframework.gradle.github.milestones;
 
+import com.google.gson.annotations.SerializedName;
+
+import java.util.Date;
+
 public class Milestone {
 	private String title;
 
 	private long number;
 
+	@SerializedName("due_on")
+	private Date dueOn;
+
 	public String getTitle() {
 		return title;
 	}
@@ -21,11 +44,20 @@ public class Milestone {
 		this.number = number;
 	}
 
+	public Date getDueOn() {
+		return dueOn;
+	}
+
+	public void setDueOn(Date dueOn) {
+		this.dueOn = dueOn;
+	}
+
 	@Override
 	public String toString() {
 		return "Milestone{" +
 				"title='" + title + '\'' +
-				", number=" + number +
+				", number='" + number + '\'' +
+				", dueOn='" + dueOn +
 				'}';
 	}
 }

+ 29 - 0
buildSrc/src/main/java/org/springframework/gradle/github/milestones/NextVersionYml.java

@@ -0,0 +1,29 @@
+/*
+ * Copyright 2019-2022 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.springframework.gradle.github.milestones;
+
+public class NextVersionYml {
+	private String version;
+
+	public String getVersion() {
+		return version;
+	}
+
+	public void setVersion(String version) {
+		this.version = version;
+	}
+}

+ 810 - 0
buildSrc/src/test/java/org/springframework/gradle/github/milestones/GitHubMilestoneApiTests.java

@@ -1,5 +1,6 @@
 package org.springframework.gradle.github.milestones;
 
+import java.time.Instant;
 import java.util.concurrent.TimeUnit;
 
 import okhttp3.mockwebserver.MockResponse;
@@ -385,4 +386,813 @@ public class GitHubMilestoneApiTests {
 		assertThat(recordedRequest.getRequestUrl().toString()).isEqualTo(this.baseUrl + "/repos/spring-projects/spring-security/issues?per_page=1&milestone=" + milestoneNumber);
 	}
 
+	@Test
+	public void isMilestoneDueTodayWhenNotFoundThenException() throws Exception {
+		String responseJson = "[\n" +
+				"   {\n" +
+				"      \"url\":\"https://api.github.com/repos/spring-projects/spring-security/milestones/207\",\n" +
+				"      \"html_url\":\"https://github.com/spring-projects/spring-security/milestone/207\",\n" +
+				"      \"labels_url\":\"https://api.github.com/repos/spring-projects/spring-security/milestones/207/labels\",\n" +
+				"      \"id\":6611880,\n" +
+				"      \"node_id\":\"MDk6TWlsZXN0b25lNjYxMTg4MA==\",\n" +
+				"      \"number\":207,\n" +
+				"      \"title\":\"5.6.x\",\n" +
+				"      \"description\":\"\",\n" +
+				"      \"creator\":{\n" +
+				"         \"login\":\"jgrandja\",\n" +
+				"         \"id\":10884212,\n" +
+				"         \"node_id\":\"MDQ6VXNlcjEwODg0MjEy\",\n" +
+				"         \"avatar_url\":\"https://avatars.githubusercontent.com/u/10884212?v=4\",\n" +
+				"         \"gravatar_id\":\"\",\n" +
+				"         \"url\":\"https://api.github.com/users/jgrandja\",\n" +
+				"         \"html_url\":\"https://github.com/jgrandja\",\n" +
+				"         \"followers_url\":\"https://api.github.com/users/jgrandja/followers\",\n" +
+				"         \"following_url\":\"https://api.github.com/users/jgrandja/following{/other_user}\",\n" +
+				"         \"gists_url\":\"https://api.github.com/users/jgrandja/gists{/gist_id}\",\n" +
+				"         \"starred_url\":\"https://api.github.com/users/jgrandja/starred{/owner}{/repo}\",\n" +
+				"         \"subscriptions_url\":\"https://api.github.com/users/jgrandja/subscriptions\",\n" +
+				"         \"organizations_url\":\"https://api.github.com/users/jgrandja/orgs\",\n" +
+				"         \"repos_url\":\"https://api.github.com/users/jgrandja/repos\",\n" +
+				"         \"events_url\":\"https://api.github.com/users/jgrandja/events{/privacy}\",\n" +
+				"         \"received_events_url\":\"https://api.github.com/users/jgrandja/received_events\",\n" +
+				"         \"type\":\"User\",\n" +
+				"         \"site_admin\":false\n" +
+				"      },\n" +
+				"      \"open_issues\":1,\n" +
+				"      \"closed_issues\":0,\n" +
+				"      \"state\":\"open\",\n" +
+				"      \"created_at\":\"2021-03-31T11:29:17Z\",\n" +
+				"      \"updated_at\":\"2021-03-31T11:30:47Z\",\n" +
+				"      \"due_on\":null,\n" +
+				"      \"closed_at\":null\n" +
+				"   },\n" +
+				"   {\n" +
+				"      \"url\":\"https://api.github.com/repos/spring-projects/spring-security/milestones/191\",\n" +
+				"      \"html_url\":\"https://github.com/spring-projects/spring-security/milestone/191\",\n" +
+				"      \"labels_url\":\"https://api.github.com/repos/spring-projects/spring-security/milestones/191/labels\",\n" +
+				"      \"id\":5884208,\n" +
+				"      \"node_id\":\"MDk6TWlsZXN0b25lNTg4NDIwOA==\",\n" +
+				"      \"number\":191,\n" +
+				"      \"title\":\"5.5.0-RC1\",\n" +
+				"      \"description\":\"\",\n" +
+				"      \"creator\":{\n" +
+				"         \"login\":\"jzheaux\",\n" +
+				"         \"id\":3627351,\n" +
+				"         \"node_id\":\"MDQ6VXNlcjM2MjczNTE=\",\n" +
+				"         \"avatar_url\":\"https://avatars.githubusercontent.com/u/3627351?v=4\",\n" +
+				"         \"gravatar_id\":\"\",\n" +
+				"         \"url\":\"https://api.github.com/users/jzheaux\",\n" +
+				"         \"html_url\":\"https://github.com/jzheaux\",\n" +
+				"         \"followers_url\":\"https://api.github.com/users/jzheaux/followers\",\n" +
+				"         \"following_url\":\"https://api.github.com/users/jzheaux/following{/other_user}\",\n" +
+				"         \"gists_url\":\"https://api.github.com/users/jzheaux/gists{/gist_id}\",\n" +
+				"         \"starred_url\":\"https://api.github.com/users/jzheaux/starred{/owner}{/repo}\",\n" +
+				"         \"subscriptions_url\":\"https://api.github.com/users/jzheaux/subscriptions\",\n" +
+				"         \"organizations_url\":\"https://api.github.com/users/jzheaux/orgs\",\n" +
+				"         \"repos_url\":\"https://api.github.com/users/jzheaux/repos\",\n" +
+				"         \"events_url\":\"https://api.github.com/users/jzheaux/events{/privacy}\",\n" +
+				"         \"received_events_url\":\"https://api.github.com/users/jzheaux/received_events\",\n" +
+				"         \"type\":\"User\",\n" +
+				"         \"site_admin\":false\n" +
+				"      },\n" +
+				"      \"open_issues\":21,\n" +
+				"      \"closed_issues\":23,\n" +
+				"      \"state\":\"open\",\n" +
+				"      \"created_at\":\"2020-09-16T13:28:03Z\",\n" +
+				"      \"updated_at\":\"2021-04-06T23:47:10Z\",\n" +
+				"      \"due_on\":\"2021-04-12T07:00:00Z\",\n" +
+				"      \"closed_at\":null\n" +
+				"   }\n" +
+				"]";
+		this.server.enqueue(new MockResponse().setBody(responseJson));
+
+		assertThatExceptionOfType(RuntimeException.class)
+				.isThrownBy(() -> this.github.isMilestoneDueToday(this.repositoryRef, "missing"));
+	}
+
+	@Test
+	public void isMilestoneDueTodayWhenPastDueThenTrue() throws Exception {
+		String responseJson = "[\n" +
+				"   {\n" +
+				"      \"url\":\"https://api.github.com/repos/spring-projects/spring-security/milestones/207\",\n" +
+				"      \"html_url\":\"https://github.com/spring-projects/spring-security/milestone/207\",\n" +
+				"      \"labels_url\":\"https://api.github.com/repos/spring-projects/spring-security/milestones/207/labels\",\n" +
+				"      \"id\":6611880,\n" +
+				"      \"node_id\":\"MDk6TWlsZXN0b25lNjYxMTg4MA==\",\n" +
+				"      \"number\":207,\n" +
+				"      \"title\":\"5.6.x\",\n" +
+				"      \"description\":\"\",\n" +
+				"      \"creator\":{\n" +
+				"         \"login\":\"jgrandja\",\n" +
+				"         \"id\":10884212,\n" +
+				"         \"node_id\":\"MDQ6VXNlcjEwODg0MjEy\",\n" +
+				"         \"avatar_url\":\"https://avatars.githubusercontent.com/u/10884212?v=4\",\n" +
+				"         \"gravatar_id\":\"\",\n" +
+				"         \"url\":\"https://api.github.com/users/jgrandja\",\n" +
+				"         \"html_url\":\"https://github.com/jgrandja\",\n" +
+				"         \"followers_url\":\"https://api.github.com/users/jgrandja/followers\",\n" +
+				"         \"following_url\":\"https://api.github.com/users/jgrandja/following{/other_user}\",\n" +
+				"         \"gists_url\":\"https://api.github.com/users/jgrandja/gists{/gist_id}\",\n" +
+				"         \"starred_url\":\"https://api.github.com/users/jgrandja/starred{/owner}{/repo}\",\n" +
+				"         \"subscriptions_url\":\"https://api.github.com/users/jgrandja/subscriptions\",\n" +
+				"         \"organizations_url\":\"https://api.github.com/users/jgrandja/orgs\",\n" +
+				"         \"repos_url\":\"https://api.github.com/users/jgrandja/repos\",\n" +
+				"         \"events_url\":\"https://api.github.com/users/jgrandja/events{/privacy}\",\n" +
+				"         \"received_events_url\":\"https://api.github.com/users/jgrandja/received_events\",\n" +
+				"         \"type\":\"User\",\n" +
+				"         \"site_admin\":false\n" +
+				"      },\n" +
+				"      \"open_issues\":1,\n" +
+				"      \"closed_issues\":0,\n" +
+				"      \"state\":\"open\",\n" +
+				"      \"created_at\":\"2021-03-31T11:29:17Z\",\n" +
+				"      \"updated_at\":\"2021-03-31T11:30:47Z\",\n" +
+				"      \"due_on\":null,\n" +
+				"      \"closed_at\":null\n" +
+				"   },\n" +
+				"   {\n" +
+				"      \"url\":\"https://api.github.com/repos/spring-projects/spring-security/milestones/191\",\n" +
+				"      \"html_url\":\"https://github.com/spring-projects/spring-security/milestone/191\",\n" +
+				"      \"labels_url\":\"https://api.github.com/repos/spring-projects/spring-security/milestones/191/labels\",\n" +
+				"      \"id\":5884208,\n" +
+				"      \"node_id\":\"MDk6TWlsZXN0b25lNTg4NDIwOA==\",\n" +
+				"      \"number\":191,\n" +
+				"      \"title\":\"5.5.0-RC1\",\n" +
+				"      \"description\":\"\",\n" +
+				"      \"creator\":{\n" +
+				"         \"login\":\"jzheaux\",\n" +
+				"         \"id\":3627351,\n" +
+				"         \"node_id\":\"MDQ6VXNlcjM2MjczNTE=\",\n" +
+				"         \"avatar_url\":\"https://avatars.githubusercontent.com/u/3627351?v=4\",\n" +
+				"         \"gravatar_id\":\"\",\n" +
+				"         \"url\":\"https://api.github.com/users/jzheaux\",\n" +
+				"         \"html_url\":\"https://github.com/jzheaux\",\n" +
+				"         \"followers_url\":\"https://api.github.com/users/jzheaux/followers\",\n" +
+				"         \"following_url\":\"https://api.github.com/users/jzheaux/following{/other_user}\",\n" +
+				"         \"gists_url\":\"https://api.github.com/users/jzheaux/gists{/gist_id}\",\n" +
+				"         \"starred_url\":\"https://api.github.com/users/jzheaux/starred{/owner}{/repo}\",\n" +
+				"         \"subscriptions_url\":\"https://api.github.com/users/jzheaux/subscriptions\",\n" +
+				"         \"organizations_url\":\"https://api.github.com/users/jzheaux/orgs\",\n" +
+				"         \"repos_url\":\"https://api.github.com/users/jzheaux/repos\",\n" +
+				"         \"events_url\":\"https://api.github.com/users/jzheaux/events{/privacy}\",\n" +
+				"         \"received_events_url\":\"https://api.github.com/users/jzheaux/received_events\",\n" +
+				"         \"type\":\"User\",\n" +
+				"         \"site_admin\":false\n" +
+				"      },\n" +
+				"      \"open_issues\":21,\n" +
+				"      \"closed_issues\":23,\n" +
+				"      \"state\":\"open\",\n" +
+				"      \"created_at\":\"2020-09-16T13:28:03Z\",\n" +
+				"      \"updated_at\":\"2021-04-06T23:47:10Z\",\n" +
+				"      \"due_on\":\"2021-04-12T07:00:00Z\",\n" +
+				"      \"closed_at\":null\n" +
+				"   }\n" +
+				"]";
+		this.server.enqueue(new MockResponse().setBody(responseJson));
+
+		boolean dueToday = this.github.isMilestoneDueToday(this.repositoryRef, "5.5.0-RC1");
+
+		RecordedRequest recordedRequest = this.server.takeRequest(1, TimeUnit.SECONDS);
+		assertThat(recordedRequest.getMethod()).isEqualToIgnoringCase("get");
+		assertThat(recordedRequest.getRequestUrl().toString()).isEqualTo(this.baseUrl + "/repos/spring-projects/spring-security/milestones?per_page=100");
+
+		assertThat(dueToday).isTrue();
+	}
+
+	@Test
+	public void isMilestoneDueTodayWhenDueTodayThenTrue() throws Exception {
+		String responseJson = "[\n" +
+				"   {\n" +
+				"      \"url\":\"https://api.github.com/repos/spring-projects/spring-security/milestones/207\",\n" +
+				"      \"html_url\":\"https://github.com/spring-projects/spring-security/milestone/207\",\n" +
+				"      \"labels_url\":\"https://api.github.com/repos/spring-projects/spring-security/milestones/207/labels\",\n" +
+				"      \"id\":6611880,\n" +
+				"      \"node_id\":\"MDk6TWlsZXN0b25lNjYxMTg4MA==\",\n" +
+				"      \"number\":207,\n" +
+				"      \"title\":\"5.6.x\",\n" +
+				"      \"description\":\"\",\n" +
+				"      \"creator\":{\n" +
+				"         \"login\":\"jgrandja\",\n" +
+				"         \"id\":10884212,\n" +
+				"         \"node_id\":\"MDQ6VXNlcjEwODg0MjEy\",\n" +
+				"         \"avatar_url\":\"https://avatars.githubusercontent.com/u/10884212?v=4\",\n" +
+				"         \"gravatar_id\":\"\",\n" +
+				"         \"url\":\"https://api.github.com/users/jgrandja\",\n" +
+				"         \"html_url\":\"https://github.com/jgrandja\",\n" +
+				"         \"followers_url\":\"https://api.github.com/users/jgrandja/followers\",\n" +
+				"         \"following_url\":\"https://api.github.com/users/jgrandja/following{/other_user}\",\n" +
+				"         \"gists_url\":\"https://api.github.com/users/jgrandja/gists{/gist_id}\",\n" +
+				"         \"starred_url\":\"https://api.github.com/users/jgrandja/starred{/owner}{/repo}\",\n" +
+				"         \"subscriptions_url\":\"https://api.github.com/users/jgrandja/subscriptions\",\n" +
+				"         \"organizations_url\":\"https://api.github.com/users/jgrandja/orgs\",\n" +
+				"         \"repos_url\":\"https://api.github.com/users/jgrandja/repos\",\n" +
+				"         \"events_url\":\"https://api.github.com/users/jgrandja/events{/privacy}\",\n" +
+				"         \"received_events_url\":\"https://api.github.com/users/jgrandja/received_events\",\n" +
+				"         \"type\":\"User\",\n" +
+				"         \"site_admin\":false\n" +
+				"      },\n" +
+				"      \"open_issues\":1,\n" +
+				"      \"closed_issues\":0,\n" +
+				"      \"state\":\"open\",\n" +
+				"      \"created_at\":\"2021-03-31T11:29:17Z\",\n" +
+				"      \"updated_at\":\"2021-03-31T11:30:47Z\",\n" +
+				"      \"due_on\":null,\n" +
+				"      \"closed_at\":null\n" +
+				"   },\n" +
+				"   {\n" +
+				"      \"url\":\"https://api.github.com/repos/spring-projects/spring-security/milestones/191\",\n" +
+				"      \"html_url\":\"https://github.com/spring-projects/spring-security/milestone/191\",\n" +
+				"      \"labels_url\":\"https://api.github.com/repos/spring-projects/spring-security/milestones/191/labels\",\n" +
+				"      \"id\":5884208,\n" +
+				"      \"node_id\":\"MDk6TWlsZXN0b25lNTg4NDIwOA==\",\n" +
+				"      \"number\":191,\n" +
+				"      \"title\":\"5.5.0-RC1\",\n" +
+				"      \"description\":\"\",\n" +
+				"      \"creator\":{\n" +
+				"         \"login\":\"jzheaux\",\n" +
+				"         \"id\":3627351,\n" +
+				"         \"node_id\":\"MDQ6VXNlcjM2MjczNTE=\",\n" +
+				"         \"avatar_url\":\"https://avatars.githubusercontent.com/u/3627351?v=4\",\n" +
+				"         \"gravatar_id\":\"\",\n" +
+				"         \"url\":\"https://api.github.com/users/jzheaux\",\n" +
+				"         \"html_url\":\"https://github.com/jzheaux\",\n" +
+				"         \"followers_url\":\"https://api.github.com/users/jzheaux/followers\",\n" +
+				"         \"following_url\":\"https://api.github.com/users/jzheaux/following{/other_user}\",\n" +
+				"         \"gists_url\":\"https://api.github.com/users/jzheaux/gists{/gist_id}\",\n" +
+				"         \"starred_url\":\"https://api.github.com/users/jzheaux/starred{/owner}{/repo}\",\n" +
+				"         \"subscriptions_url\":\"https://api.github.com/users/jzheaux/subscriptions\",\n" +
+				"         \"organizations_url\":\"https://api.github.com/users/jzheaux/orgs\",\n" +
+				"         \"repos_url\":\"https://api.github.com/users/jzheaux/repos\",\n" +
+				"         \"events_url\":\"https://api.github.com/users/jzheaux/events{/privacy}\",\n" +
+				"         \"received_events_url\":\"https://api.github.com/users/jzheaux/received_events\",\n" +
+				"         \"type\":\"User\",\n" +
+				"         \"site_admin\":false\n" +
+				"      },\n" +
+				"      \"open_issues\":21,\n" +
+				"      \"closed_issues\":23,\n" +
+				"      \"state\":\"open\",\n" +
+				"      \"created_at\":\"2020-09-16T13:28:03Z\",\n" +
+				"      \"updated_at\":\"2021-04-06T23:47:10Z\",\n" +
+				"      \"due_on\":\"" + Instant.now().toString() + "\",\n" +
+				"      \"closed_at\":null\n" +
+				"   }\n" +
+				"]";
+		this.server.enqueue(new MockResponse().setBody(responseJson));
+
+		boolean dueToday = this.github.isMilestoneDueToday(this.repositoryRef, "5.5.0-RC1");
+
+		RecordedRequest recordedRequest = this.server.takeRequest(1, TimeUnit.SECONDS);
+		assertThat(recordedRequest.getMethod()).isEqualToIgnoringCase("get");
+		assertThat(recordedRequest.getRequestUrl().toString()).isEqualTo(this.baseUrl + "/repos/spring-projects/spring-security/milestones?per_page=100");
+
+		assertThat(dueToday).isTrue();
+	}
+
+	@Test
+	public void isMilestoneDueTodayWhenNoDueDateThenFalse() throws Exception {
+		String responseJson = "[\n" +
+				"   {\n" +
+				"      \"url\":\"https://api.github.com/repos/spring-projects/spring-security/milestones/207\",\n" +
+				"      \"html_url\":\"https://github.com/spring-projects/spring-security/milestone/207\",\n" +
+				"      \"labels_url\":\"https://api.github.com/repos/spring-projects/spring-security/milestones/207/labels\",\n" +
+				"      \"id\":6611880,\n" +
+				"      \"node_id\":\"MDk6TWlsZXN0b25lNjYxMTg4MA==\",\n" +
+				"      \"number\":207,\n" +
+				"      \"title\":\"5.6.x\",\n" +
+				"      \"description\":\"\",\n" +
+				"      \"creator\":{\n" +
+				"         \"login\":\"jgrandja\",\n" +
+				"         \"id\":10884212,\n" +
+				"         \"node_id\":\"MDQ6VXNlcjEwODg0MjEy\",\n" +
+				"         \"avatar_url\":\"https://avatars.githubusercontent.com/u/10884212?v=4\",\n" +
+				"         \"gravatar_id\":\"\",\n" +
+				"         \"url\":\"https://api.github.com/users/jgrandja\",\n" +
+				"         \"html_url\":\"https://github.com/jgrandja\",\n" +
+				"         \"followers_url\":\"https://api.github.com/users/jgrandja/followers\",\n" +
+				"         \"following_url\":\"https://api.github.com/users/jgrandja/following{/other_user}\",\n" +
+				"         \"gists_url\":\"https://api.github.com/users/jgrandja/gists{/gist_id}\",\n" +
+				"         \"starred_url\":\"https://api.github.com/users/jgrandja/starred{/owner}{/repo}\",\n" +
+				"         \"subscriptions_url\":\"https://api.github.com/users/jgrandja/subscriptions\",\n" +
+				"         \"organizations_url\":\"https://api.github.com/users/jgrandja/orgs\",\n" +
+				"         \"repos_url\":\"https://api.github.com/users/jgrandja/repos\",\n" +
+				"         \"events_url\":\"https://api.github.com/users/jgrandja/events{/privacy}\",\n" +
+				"         \"received_events_url\":\"https://api.github.com/users/jgrandja/received_events\",\n" +
+				"         \"type\":\"User\",\n" +
+				"         \"site_admin\":false\n" +
+				"      },\n" +
+				"      \"open_issues\":1,\n" +
+				"      \"closed_issues\":0,\n" +
+				"      \"state\":\"open\",\n" +
+				"      \"created_at\":\"2021-03-31T11:29:17Z\",\n" +
+				"      \"updated_at\":\"2021-03-31T11:30:47Z\",\n" +
+				"      \"due_on\":null,\n" +
+				"      \"closed_at\":null\n" +
+				"   },\n" +
+				"   {\n" +
+				"      \"url\":\"https://api.github.com/repos/spring-projects/spring-security/milestones/191\",\n" +
+				"      \"html_url\":\"https://github.com/spring-projects/spring-security/milestone/191\",\n" +
+				"      \"labels_url\":\"https://api.github.com/repos/spring-projects/spring-security/milestones/191/labels\",\n" +
+				"      \"id\":5884208,\n" +
+				"      \"node_id\":\"MDk6TWlsZXN0b25lNTg4NDIwOA==\",\n" +
+				"      \"number\":191,\n" +
+				"      \"title\":\"5.5.0-RC1\",\n" +
+				"      \"description\":\"\",\n" +
+				"      \"creator\":{\n" +
+				"         \"login\":\"jzheaux\",\n" +
+				"         \"id\":3627351,\n" +
+				"         \"node_id\":\"MDQ6VXNlcjM2MjczNTE=\",\n" +
+				"         \"avatar_url\":\"https://avatars.githubusercontent.com/u/3627351?v=4\",\n" +
+				"         \"gravatar_id\":\"\",\n" +
+				"         \"url\":\"https://api.github.com/users/jzheaux\",\n" +
+				"         \"html_url\":\"https://github.com/jzheaux\",\n" +
+				"         \"followers_url\":\"https://api.github.com/users/jzheaux/followers\",\n" +
+				"         \"following_url\":\"https://api.github.com/users/jzheaux/following{/other_user}\",\n" +
+				"         \"gists_url\":\"https://api.github.com/users/jzheaux/gists{/gist_id}\",\n" +
+				"         \"starred_url\":\"https://api.github.com/users/jzheaux/starred{/owner}{/repo}\",\n" +
+				"         \"subscriptions_url\":\"https://api.github.com/users/jzheaux/subscriptions\",\n" +
+				"         \"organizations_url\":\"https://api.github.com/users/jzheaux/orgs\",\n" +
+				"         \"repos_url\":\"https://api.github.com/users/jzheaux/repos\",\n" +
+				"         \"events_url\":\"https://api.github.com/users/jzheaux/events{/privacy}\",\n" +
+				"         \"received_events_url\":\"https://api.github.com/users/jzheaux/received_events\",\n" +
+				"         \"type\":\"User\",\n" +
+				"         \"site_admin\":false\n" +
+				"      },\n" +
+				"      \"open_issues\":21,\n" +
+				"      \"closed_issues\":23,\n" +
+				"      \"state\":\"open\",\n" +
+				"      \"created_at\":\"2020-09-16T13:28:03Z\",\n" +
+				"      \"updated_at\":\"2021-04-06T23:47:10Z\",\n" +
+				"      \"due_on\":null,\n" +
+				"      \"closed_at\":null\n" +
+				"   }\n" +
+				"]";
+		this.server.enqueue(new MockResponse().setBody(responseJson));
+
+		boolean dueToday = this.github.isMilestoneDueToday(this.repositoryRef, "5.5.0-RC1");
+
+		RecordedRequest recordedRequest = this.server.takeRequest(1, TimeUnit.SECONDS);
+		assertThat(recordedRequest.getMethod()).isEqualToIgnoringCase("get");
+		assertThat(recordedRequest.getRequestUrl().toString()).isEqualTo(this.baseUrl + "/repos/spring-projects/spring-security/milestones?per_page=100");
+
+		assertThat(dueToday).isFalse();
+	}
+
+	@Test
+	public void isMilestoneDueTodayWhenDueDateInFutureThenFalse() throws Exception {
+		String responseJson = "[\n" +
+				"   {\n" +
+				"      \"url\":\"https://api.github.com/repos/spring-projects/spring-security/milestones/207\",\n" +
+				"      \"html_url\":\"https://github.com/spring-projects/spring-security/milestone/207\",\n" +
+				"      \"labels_url\":\"https://api.github.com/repos/spring-projects/spring-security/milestones/207/labels\",\n" +
+				"      \"id\":6611880,\n" +
+				"      \"node_id\":\"MDk6TWlsZXN0b25lNjYxMTg4MA==\",\n" +
+				"      \"number\":207,\n" +
+				"      \"title\":\"5.6.x\",\n" +
+				"      \"description\":\"\",\n" +
+				"      \"creator\":{\n" +
+				"         \"login\":\"jgrandja\",\n" +
+				"         \"id\":10884212,\n" +
+				"         \"node_id\":\"MDQ6VXNlcjEwODg0MjEy\",\n" +
+				"         \"avatar_url\":\"https://avatars.githubusercontent.com/u/10884212?v=4\",\n" +
+				"         \"gravatar_id\":\"\",\n" +
+				"         \"url\":\"https://api.github.com/users/jgrandja\",\n" +
+				"         \"html_url\":\"https://github.com/jgrandja\",\n" +
+				"         \"followers_url\":\"https://api.github.com/users/jgrandja/followers\",\n" +
+				"         \"following_url\":\"https://api.github.com/users/jgrandja/following{/other_user}\",\n" +
+				"         \"gists_url\":\"https://api.github.com/users/jgrandja/gists{/gist_id}\",\n" +
+				"         \"starred_url\":\"https://api.github.com/users/jgrandja/starred{/owner}{/repo}\",\n" +
+				"         \"subscriptions_url\":\"https://api.github.com/users/jgrandja/subscriptions\",\n" +
+				"         \"organizations_url\":\"https://api.github.com/users/jgrandja/orgs\",\n" +
+				"         \"repos_url\":\"https://api.github.com/users/jgrandja/repos\",\n" +
+				"         \"events_url\":\"https://api.github.com/users/jgrandja/events{/privacy}\",\n" +
+				"         \"received_events_url\":\"https://api.github.com/users/jgrandja/received_events\",\n" +
+				"         \"type\":\"User\",\n" +
+				"         \"site_admin\":false\n" +
+				"      },\n" +
+				"      \"open_issues\":1,\n" +
+				"      \"closed_issues\":0,\n" +
+				"      \"state\":\"open\",\n" +
+				"      \"created_at\":\"2021-03-31T11:29:17Z\",\n" +
+				"      \"updated_at\":\"2021-03-31T11:30:47Z\",\n" +
+				"      \"due_on\":null,\n" +
+				"      \"closed_at\":null\n" +
+				"   },\n" +
+				"   {\n" +
+				"      \"url\":\"https://api.github.com/repos/spring-projects/spring-security/milestones/191\",\n" +
+				"      \"html_url\":\"https://github.com/spring-projects/spring-security/milestone/191\",\n" +
+				"      \"labels_url\":\"https://api.github.com/repos/spring-projects/spring-security/milestones/191/labels\",\n" +
+				"      \"id\":5884208,\n" +
+				"      \"node_id\":\"MDk6TWlsZXN0b25lNTg4NDIwOA==\",\n" +
+				"      \"number\":191,\n" +
+				"      \"title\":\"5.5.0-RC1\",\n" +
+				"      \"description\":\"\",\n" +
+				"      \"creator\":{\n" +
+				"         \"login\":\"jzheaux\",\n" +
+				"         \"id\":3627351,\n" +
+				"         \"node_id\":\"MDQ6VXNlcjM2MjczNTE=\",\n" +
+				"         \"avatar_url\":\"https://avatars.githubusercontent.com/u/3627351?v=4\",\n" +
+				"         \"gravatar_id\":\"\",\n" +
+				"         \"url\":\"https://api.github.com/users/jzheaux\",\n" +
+				"         \"html_url\":\"https://github.com/jzheaux\",\n" +
+				"         \"followers_url\":\"https://api.github.com/users/jzheaux/followers\",\n" +
+				"         \"following_url\":\"https://api.github.com/users/jzheaux/following{/other_user}\",\n" +
+				"         \"gists_url\":\"https://api.github.com/users/jzheaux/gists{/gist_id}\",\n" +
+				"         \"starred_url\":\"https://api.github.com/users/jzheaux/starred{/owner}{/repo}\",\n" +
+				"         \"subscriptions_url\":\"https://api.github.com/users/jzheaux/subscriptions\",\n" +
+				"         \"organizations_url\":\"https://api.github.com/users/jzheaux/orgs\",\n" +
+				"         \"repos_url\":\"https://api.github.com/users/jzheaux/repos\",\n" +
+				"         \"events_url\":\"https://api.github.com/users/jzheaux/events{/privacy}\",\n" +
+				"         \"received_events_url\":\"https://api.github.com/users/jzheaux/received_events\",\n" +
+				"         \"type\":\"User\",\n" +
+				"         \"site_admin\":false\n" +
+				"      },\n" +
+				"      \"open_issues\":21,\n" +
+				"      \"closed_issues\":23,\n" +
+				"      \"state\":\"open\",\n" +
+				"      \"created_at\":\"2020-09-16T13:28:03Z\",\n" +
+				"      \"updated_at\":\"2021-04-06T23:47:10Z\",\n" +
+				"      \"due_on\":\"3000-04-12T07:00:00Z\",\n" +
+				"      \"closed_at\":null\n" +
+				"   }\n" +
+				"]";
+		this.server.enqueue(new MockResponse().setBody(responseJson));
+
+		boolean dueToday = this.github.isMilestoneDueToday(this.repositoryRef, "5.5.0-RC1");
+
+		RecordedRequest recordedRequest = this.server.takeRequest(1, TimeUnit.SECONDS);
+		assertThat(recordedRequest.getMethod()).isEqualToIgnoringCase("get");
+		assertThat(recordedRequest.getRequestUrl().toString()).isEqualTo(this.baseUrl + "/repos/spring-projects/spring-security/milestones?per_page=100");
+
+		assertThat(dueToday).isFalse();
+	}
+
+	@Test
+	public void calculateNextReleaseMilestoneWhenCurrentVersionIsNotSnapshotThenException() {
+		assertThatExceptionOfType(RuntimeException.class)
+				.isThrownBy(() -> this.github.getNextReleaseMilestone(this.repositoryRef, "5.5.0-RC1"));
+	}
+
+	@Test
+	public void calculateNextReleaseMilestoneWhenPatchSegmentGreaterThan0ThenReturnsVersionWithoutSnapshot() {
+		String nextVersion = this.github.getNextReleaseMilestone(this.repositoryRef, "5.5.1-SNAPSHOT");
+
+		assertThat(nextVersion).isEqualTo("5.5.1");
+	}
+
+	@Test
+	public void calculateNextReleaseMilestoneWhenMilestoneAndRcExistThenReturnsMilestone() throws Exception {
+		String responseJson = "[\n" +
+				"   {\n" +
+				"      \"url\":\"https://api.github.com/repos/spring-projects/spring-security/milestones/207\",\n" +
+				"      \"html_url\":\"https://github.com/spring-projects/spring-security/milestone/207\",\n" +
+				"      \"labels_url\":\"https://api.github.com/repos/spring-projects/spring-security/milestones/207/labels\",\n" +
+				"      \"id\":6611880,\n" +
+				"      \"node_id\":\"MDk6TWlsZXN0b25lNjYxMTg4MA==\",\n" +
+				"      \"number\":207,\n" +
+				"      \"title\":\"5.5.0-M1\",\n" +
+				"      \"description\":\"\",\n" +
+				"      \"creator\":{\n" +
+				"         \"login\":\"jgrandja\",\n" +
+				"         \"id\":10884212,\n" +
+				"         \"node_id\":\"MDQ6VXNlcjEwODg0MjEy\",\n" +
+				"         \"avatar_url\":\"https://avatars.githubusercontent.com/u/10884212?v=4\",\n" +
+				"         \"gravatar_id\":\"\",\n" +
+				"         \"url\":\"https://api.github.com/users/jgrandja\",\n" +
+				"         \"html_url\":\"https://github.com/jgrandja\",\n" +
+				"         \"followers_url\":\"https://api.github.com/users/jgrandja/followers\",\n" +
+				"         \"following_url\":\"https://api.github.com/users/jgrandja/following{/other_user}\",\n" +
+				"         \"gists_url\":\"https://api.github.com/users/jgrandja/gists{/gist_id}\",\n" +
+				"         \"starred_url\":\"https://api.github.com/users/jgrandja/starred{/owner}{/repo}\",\n" +
+				"         \"subscriptions_url\":\"https://api.github.com/users/jgrandja/subscriptions\",\n" +
+				"         \"organizations_url\":\"https://api.github.com/users/jgrandja/orgs\",\n" +
+				"         \"repos_url\":\"https://api.github.com/users/jgrandja/repos\",\n" +
+				"         \"events_url\":\"https://api.github.com/users/jgrandja/events{/privacy}\",\n" +
+				"         \"received_events_url\":\"https://api.github.com/users/jgrandja/received_events\",\n" +
+				"         \"type\":\"User\",\n" +
+				"         \"site_admin\":false\n" +
+				"      },\n" +
+				"      \"open_issues\":1,\n" +
+				"      \"closed_issues\":0,\n" +
+				"      \"state\":\"open\",\n" +
+				"      \"created_at\":\"2021-03-31T11:29:17Z\",\n" +
+				"      \"updated_at\":\"2021-03-31T11:30:47Z\",\n" +
+				"      \"due_on\":null,\n" +
+				"      \"closed_at\":null\n" +
+				"   },\n" +
+				"   {\n" +
+				"      \"url\":\"https://api.github.com/repos/spring-projects/spring-security/milestones/191\",\n" +
+				"      \"html_url\":\"https://github.com/spring-projects/spring-security/milestone/191\",\n" +
+				"      \"labels_url\":\"https://api.github.com/repos/spring-projects/spring-security/milestones/191/labels\",\n" +
+				"      \"id\":5884208,\n" +
+				"      \"node_id\":\"MDk6TWlsZXN0b25lNTg4NDIwOA==\",\n" +
+				"      \"number\":191,\n" +
+				"      \"title\":\"5.5.0-RC1\",\n" +
+				"      \"description\":\"\",\n" +
+				"      \"creator\":{\n" +
+				"         \"login\":\"jzheaux\",\n" +
+				"         \"id\":3627351,\n" +
+				"         \"node_id\":\"MDQ6VXNlcjM2MjczNTE=\",\n" +
+				"         \"avatar_url\":\"https://avatars.githubusercontent.com/u/3627351?v=4\",\n" +
+				"         \"gravatar_id\":\"\",\n" +
+				"         \"url\":\"https://api.github.com/users/jzheaux\",\n" +
+				"         \"html_url\":\"https://github.com/jzheaux\",\n" +
+				"         \"followers_url\":\"https://api.github.com/users/jzheaux/followers\",\n" +
+				"         \"following_url\":\"https://api.github.com/users/jzheaux/following{/other_user}\",\n" +
+				"         \"gists_url\":\"https://api.github.com/users/jzheaux/gists{/gist_id}\",\n" +
+				"         \"starred_url\":\"https://api.github.com/users/jzheaux/starred{/owner}{/repo}\",\n" +
+				"         \"subscriptions_url\":\"https://api.github.com/users/jzheaux/subscriptions\",\n" +
+				"         \"organizations_url\":\"https://api.github.com/users/jzheaux/orgs\",\n" +
+				"         \"repos_url\":\"https://api.github.com/users/jzheaux/repos\",\n" +
+				"         \"events_url\":\"https://api.github.com/users/jzheaux/events{/privacy}\",\n" +
+				"         \"received_events_url\":\"https://api.github.com/users/jzheaux/received_events\",\n" +
+				"         \"type\":\"User\",\n" +
+				"         \"site_admin\":false\n" +
+				"      },\n" +
+				"      \"open_issues\":21,\n" +
+				"      \"closed_issues\":23,\n" +
+				"      \"state\":\"open\",\n" +
+				"      \"created_at\":\"2020-09-16T13:28:03Z\",\n" +
+				"      \"updated_at\":\"2021-04-06T23:47:10Z\",\n" +
+				"      \"due_on\":\"3000-04-12T07:00:00Z\",\n" +
+				"      \"closed_at\":null\n" +
+				"   }\n" +
+				"]";
+		this.server.enqueue(new MockResponse().setBody(responseJson));
+
+		String nextVersion = this.github.getNextReleaseMilestone(this.repositoryRef, "5.5.0-SNAPSHOT");
+
+		RecordedRequest recordedRequest = this.server.takeRequest(1, TimeUnit.SECONDS);
+		assertThat(recordedRequest.getMethod()).isEqualToIgnoringCase("get");
+		assertThat(recordedRequest.getRequestUrl().toString()).isEqualTo(this.baseUrl + "/repos/spring-projects/spring-security/milestones?per_page=100");
+
+		assertThat(nextVersion).isEqualTo("5.5.0-M1");
+	}
+
+	@Test
+	public void calculateNextReleaseMilestoneWhenTwoMilestonesExistThenReturnsSmallerMilestone() throws Exception {
+		String responseJson = "[\n" +
+				"   {\n" +
+				"      \"url\":\"https://api.github.com/repos/spring-projects/spring-security/milestones/207\",\n" +
+				"      \"html_url\":\"https://github.com/spring-projects/spring-security/milestone/207\",\n" +
+				"      \"labels_url\":\"https://api.github.com/repos/spring-projects/spring-security/milestones/207/labels\",\n" +
+				"      \"id\":6611880,\n" +
+				"      \"node_id\":\"MDk6TWlsZXN0b25lNjYxMTg4MA==\",\n" +
+				"      \"number\":207,\n" +
+				"      \"title\":\"5.5.0-M9\",\n" +
+				"      \"description\":\"\",\n" +
+				"      \"creator\":{\n" +
+				"         \"login\":\"jgrandja\",\n" +
+				"         \"id\":10884212,\n" +
+				"         \"node_id\":\"MDQ6VXNlcjEwODg0MjEy\",\n" +
+				"         \"avatar_url\":\"https://avatars.githubusercontent.com/u/10884212?v=4\",\n" +
+				"         \"gravatar_id\":\"\",\n" +
+				"         \"url\":\"https://api.github.com/users/jgrandja\",\n" +
+				"         \"html_url\":\"https://github.com/jgrandja\",\n" +
+				"         \"followers_url\":\"https://api.github.com/users/jgrandja/followers\",\n" +
+				"         \"following_url\":\"https://api.github.com/users/jgrandja/following{/other_user}\",\n" +
+				"         \"gists_url\":\"https://api.github.com/users/jgrandja/gists{/gist_id}\",\n" +
+				"         \"starred_url\":\"https://api.github.com/users/jgrandja/starred{/owner}{/repo}\",\n" +
+				"         \"subscriptions_url\":\"https://api.github.com/users/jgrandja/subscriptions\",\n" +
+				"         \"organizations_url\":\"https://api.github.com/users/jgrandja/orgs\",\n" +
+				"         \"repos_url\":\"https://api.github.com/users/jgrandja/repos\",\n" +
+				"         \"events_url\":\"https://api.github.com/users/jgrandja/events{/privacy}\",\n" +
+				"         \"received_events_url\":\"https://api.github.com/users/jgrandja/received_events\",\n" +
+				"         \"type\":\"User\",\n" +
+				"         \"site_admin\":false\n" +
+				"      },\n" +
+				"      \"open_issues\":1,\n" +
+				"      \"closed_issues\":0,\n" +
+				"      \"state\":\"open\",\n" +
+				"      \"created_at\":\"2021-03-31T11:29:17Z\",\n" +
+				"      \"updated_at\":\"2021-03-31T11:30:47Z\",\n" +
+				"      \"due_on\":null,\n" +
+				"      \"closed_at\":null\n" +
+				"   },\n" +
+				"   {\n" +
+				"      \"url\":\"https://api.github.com/repos/spring-projects/spring-security/milestones/191\",\n" +
+				"      \"html_url\":\"https://github.com/spring-projects/spring-security/milestone/191\",\n" +
+				"      \"labels_url\":\"https://api.github.com/repos/spring-projects/spring-security/milestones/191/labels\",\n" +
+				"      \"id\":5884208,\n" +
+				"      \"node_id\":\"MDk6TWlsZXN0b25lNTg4NDIwOA==\",\n" +
+				"      \"number\":191,\n" +
+				"      \"title\":\"5.5.0-M10\",\n" +
+				"      \"description\":\"\",\n" +
+				"      \"creator\":{\n" +
+				"         \"login\":\"jzheaux\",\n" +
+				"         \"id\":3627351,\n" +
+				"         \"node_id\":\"MDQ6VXNlcjM2MjczNTE=\",\n" +
+				"         \"avatar_url\":\"https://avatars.githubusercontent.com/u/3627351?v=4\",\n" +
+				"         \"gravatar_id\":\"\",\n" +
+				"         \"url\":\"https://api.github.com/users/jzheaux\",\n" +
+				"         \"html_url\":\"https://github.com/jzheaux\",\n" +
+				"         \"followers_url\":\"https://api.github.com/users/jzheaux/followers\",\n" +
+				"         \"following_url\":\"https://api.github.com/users/jzheaux/following{/other_user}\",\n" +
+				"         \"gists_url\":\"https://api.github.com/users/jzheaux/gists{/gist_id}\",\n" +
+				"         \"starred_url\":\"https://api.github.com/users/jzheaux/starred{/owner}{/repo}\",\n" +
+				"         \"subscriptions_url\":\"https://api.github.com/users/jzheaux/subscriptions\",\n" +
+				"         \"organizations_url\":\"https://api.github.com/users/jzheaux/orgs\",\n" +
+				"         \"repos_url\":\"https://api.github.com/users/jzheaux/repos\",\n" +
+				"         \"events_url\":\"https://api.github.com/users/jzheaux/events{/privacy}\",\n" +
+				"         \"received_events_url\":\"https://api.github.com/users/jzheaux/received_events\",\n" +
+				"         \"type\":\"User\",\n" +
+				"         \"site_admin\":false\n" +
+				"      },\n" +
+				"      \"open_issues\":21,\n" +
+				"      \"closed_issues\":23,\n" +
+				"      \"state\":\"open\",\n" +
+				"      \"created_at\":\"2020-09-16T13:28:03Z\",\n" +
+				"      \"updated_at\":\"2021-04-06T23:47:10Z\",\n" +
+				"      \"due_on\":\"3000-04-12T07:00:00Z\",\n" +
+				"      \"closed_at\":null\n" +
+				"   }\n" +
+				"]";
+		this.server.enqueue(new MockResponse().setBody(responseJson));
+
+		String nextVersion = this.github.getNextReleaseMilestone(this.repositoryRef, "5.5.0-SNAPSHOT");
+
+		RecordedRequest recordedRequest = this.server.takeRequest(1, TimeUnit.SECONDS);
+		assertThat(recordedRequest.getMethod()).isEqualToIgnoringCase("get");
+		assertThat(recordedRequest.getRequestUrl().toString()).isEqualTo(this.baseUrl + "/repos/spring-projects/spring-security/milestones?per_page=100");
+
+		assertThat(nextVersion).isEqualTo("5.5.0-M9");
+	}
+
+	@Test
+	public void calculateNextReleaseMilestoneWhenTwoRcsExistThenReturnsSmallerRc() throws Exception {
+		String responseJson = "[\n" +
+				"   {\n" +
+				"      \"url\":\"https://api.github.com/repos/spring-projects/spring-security/milestones/207\",\n" +
+				"      \"html_url\":\"https://github.com/spring-projects/spring-security/milestone/207\",\n" +
+				"      \"labels_url\":\"https://api.github.com/repos/spring-projects/spring-security/milestones/207/labels\",\n" +
+				"      \"id\":6611880,\n" +
+				"      \"node_id\":\"MDk6TWlsZXN0b25lNjYxMTg4MA==\",\n" +
+				"      \"number\":207,\n" +
+				"      \"title\":\"5.5.0-RC9\",\n" +
+				"      \"description\":\"\",\n" +
+				"      \"creator\":{\n" +
+				"         \"login\":\"jgrandja\",\n" +
+				"         \"id\":10884212,\n" +
+				"         \"node_id\":\"MDQ6VXNlcjEwODg0MjEy\",\n" +
+				"         \"avatar_url\":\"https://avatars.githubusercontent.com/u/10884212?v=4\",\n" +
+				"         \"gravatar_id\":\"\",\n" +
+				"         \"url\":\"https://api.github.com/users/jgrandja\",\n" +
+				"         \"html_url\":\"https://github.com/jgrandja\",\n" +
+				"         \"followers_url\":\"https://api.github.com/users/jgrandja/followers\",\n" +
+				"         \"following_url\":\"https://api.github.com/users/jgrandja/following{/other_user}\",\n" +
+				"         \"gists_url\":\"https://api.github.com/users/jgrandja/gists{/gist_id}\",\n" +
+				"         \"starred_url\":\"https://api.github.com/users/jgrandja/starred{/owner}{/repo}\",\n" +
+				"         \"subscriptions_url\":\"https://api.github.com/users/jgrandja/subscriptions\",\n" +
+				"         \"organizations_url\":\"https://api.github.com/users/jgrandja/orgs\",\n" +
+				"         \"repos_url\":\"https://api.github.com/users/jgrandja/repos\",\n" +
+				"         \"events_url\":\"https://api.github.com/users/jgrandja/events{/privacy}\",\n" +
+				"         \"received_events_url\":\"https://api.github.com/users/jgrandja/received_events\",\n" +
+				"         \"type\":\"User\",\n" +
+				"         \"site_admin\":false\n" +
+				"      },\n" +
+				"      \"open_issues\":1,\n" +
+				"      \"closed_issues\":0,\n" +
+				"      \"state\":\"open\",\n" +
+				"      \"created_at\":\"2021-03-31T11:29:17Z\",\n" +
+				"      \"updated_at\":\"2021-03-31T11:30:47Z\",\n" +
+				"      \"due_on\":null,\n" +
+				"      \"closed_at\":null\n" +
+				"   },\n" +
+				"   {\n" +
+				"      \"url\":\"https://api.github.com/repos/spring-projects/spring-security/milestones/191\",\n" +
+				"      \"html_url\":\"https://github.com/spring-projects/spring-security/milestone/191\",\n" +
+				"      \"labels_url\":\"https://api.github.com/repos/spring-projects/spring-security/milestones/191/labels\",\n" +
+				"      \"id\":5884208,\n" +
+				"      \"node_id\":\"MDk6TWlsZXN0b25lNTg4NDIwOA==\",\n" +
+				"      \"number\":191,\n" +
+				"      \"title\":\"5.5.0-RC10\",\n" +
+				"      \"description\":\"\",\n" +
+				"      \"creator\":{\n" +
+				"         \"login\":\"jzheaux\",\n" +
+				"         \"id\":3627351,\n" +
+				"         \"node_id\":\"MDQ6VXNlcjM2MjczNTE=\",\n" +
+				"         \"avatar_url\":\"https://avatars.githubusercontent.com/u/3627351?v=4\",\n" +
+				"         \"gravatar_id\":\"\",\n" +
+				"         \"url\":\"https://api.github.com/users/jzheaux\",\n" +
+				"         \"html_url\":\"https://github.com/jzheaux\",\n" +
+				"         \"followers_url\":\"https://api.github.com/users/jzheaux/followers\",\n" +
+				"         \"following_url\":\"https://api.github.com/users/jzheaux/following{/other_user}\",\n" +
+				"         \"gists_url\":\"https://api.github.com/users/jzheaux/gists{/gist_id}\",\n" +
+				"         \"starred_url\":\"https://api.github.com/users/jzheaux/starred{/owner}{/repo}\",\n" +
+				"         \"subscriptions_url\":\"https://api.github.com/users/jzheaux/subscriptions\",\n" +
+				"         \"organizations_url\":\"https://api.github.com/users/jzheaux/orgs\",\n" +
+				"         \"repos_url\":\"https://api.github.com/users/jzheaux/repos\",\n" +
+				"         \"events_url\":\"https://api.github.com/users/jzheaux/events{/privacy}\",\n" +
+				"         \"received_events_url\":\"https://api.github.com/users/jzheaux/received_events\",\n" +
+				"         \"type\":\"User\",\n" +
+				"         \"site_admin\":false\n" +
+				"      },\n" +
+				"      \"open_issues\":21,\n" +
+				"      \"closed_issues\":23,\n" +
+				"      \"state\":\"open\",\n" +
+				"      \"created_at\":\"2020-09-16T13:28:03Z\",\n" +
+				"      \"updated_at\":\"2021-04-06T23:47:10Z\",\n" +
+				"      \"due_on\":\"3000-04-12T07:00:00Z\",\n" +
+				"      \"closed_at\":null\n" +
+				"   }\n" +
+				"]";
+		this.server.enqueue(new MockResponse().setBody(responseJson));
+
+		String nextVersion = this.github.getNextReleaseMilestone(this.repositoryRef, "5.5.0-SNAPSHOT");
+
+		RecordedRequest recordedRequest = this.server.takeRequest(1, TimeUnit.SECONDS);
+		assertThat(recordedRequest.getMethod()).isEqualToIgnoringCase("get");
+		assertThat(recordedRequest.getRequestUrl().toString()).isEqualTo(this.baseUrl + "/repos/spring-projects/spring-security/milestones?per_page=100");
+
+		assertThat(nextVersion).isEqualTo("5.5.0-RC9");
+	}
+
+	@Test
+	public void calculateNextReleaseMilestoneWhenNoPreReleaseThenReturnsVersionWithoutSnapshot() throws Exception {
+		String responseJson = "[\n" +
+				"   {\n" +
+				"      \"url\":\"https://api.github.com/repos/spring-projects/spring-security/milestones/207\",\n" +
+				"      \"html_url\":\"https://github.com/spring-projects/spring-security/milestone/207\",\n" +
+				"      \"labels_url\":\"https://api.github.com/repos/spring-projects/spring-security/milestones/207/labels\",\n" +
+				"      \"id\":6611880,\n" +
+				"      \"node_id\":\"MDk6TWlsZXN0b25lNjYxMTg4MA==\",\n" +
+				"      \"number\":207,\n" +
+				"      \"title\":\"5.6.x\",\n" +
+				"      \"description\":\"\",\n" +
+				"      \"creator\":{\n" +
+				"         \"login\":\"jgrandja\",\n" +
+				"         \"id\":10884212,\n" +
+				"         \"node_id\":\"MDQ6VXNlcjEwODg0MjEy\",\n" +
+				"         \"avatar_url\":\"https://avatars.githubusercontent.com/u/10884212?v=4\",\n" +
+				"         \"gravatar_id\":\"\",\n" +
+				"         \"url\":\"https://api.github.com/users/jgrandja\",\n" +
+				"         \"html_url\":\"https://github.com/jgrandja\",\n" +
+				"         \"followers_url\":\"https://api.github.com/users/jgrandja/followers\",\n" +
+				"         \"following_url\":\"https://api.github.com/users/jgrandja/following{/other_user}\",\n" +
+				"         \"gists_url\":\"https://api.github.com/users/jgrandja/gists{/gist_id}\",\n" +
+				"         \"starred_url\":\"https://api.github.com/users/jgrandja/starred{/owner}{/repo}\",\n" +
+				"         \"subscriptions_url\":\"https://api.github.com/users/jgrandja/subscriptions\",\n" +
+				"         \"organizations_url\":\"https://api.github.com/users/jgrandja/orgs\",\n" +
+				"         \"repos_url\":\"https://api.github.com/users/jgrandja/repos\",\n" +
+				"         \"events_url\":\"https://api.github.com/users/jgrandja/events{/privacy}\",\n" +
+				"         \"received_events_url\":\"https://api.github.com/users/jgrandja/received_events\",\n" +
+				"         \"type\":\"User\",\n" +
+				"         \"site_admin\":false\n" +
+				"      },\n" +
+				"      \"open_issues\":1,\n" +
+				"      \"closed_issues\":0,\n" +
+				"      \"state\":\"open\",\n" +
+				"      \"created_at\":\"2021-03-31T11:29:17Z\",\n" +
+				"      \"updated_at\":\"2021-03-31T11:30:47Z\",\n" +
+				"      \"due_on\":null,\n" +
+				"      \"closed_at\":null\n" +
+				"   },\n" +
+				"   {\n" +
+				"      \"url\":\"https://api.github.com/repos/spring-projects/spring-security/milestones/191\",\n" +
+				"      \"html_url\":\"https://github.com/spring-projects/spring-security/milestone/191\",\n" +
+				"      \"labels_url\":\"https://api.github.com/repos/spring-projects/spring-security/milestones/191/labels\",\n" +
+				"      \"id\":5884208,\n" +
+				"      \"node_id\":\"MDk6TWlsZXN0b25lNTg4NDIwOA==\",\n" +
+				"      \"number\":191,\n" +
+				"      \"title\":\"5.4.3\",\n" +
+				"      \"description\":\"\",\n" +
+				"      \"creator\":{\n" +
+				"         \"login\":\"jzheaux\",\n" +
+				"         \"id\":3627351,\n" +
+				"         \"node_id\":\"MDQ6VXNlcjM2MjczNTE=\",\n" +
+				"         \"avatar_url\":\"https://avatars.githubusercontent.com/u/3627351?v=4\",\n" +
+				"         \"gravatar_id\":\"\",\n" +
+				"         \"url\":\"https://api.github.com/users/jzheaux\",\n" +
+				"         \"html_url\":\"https://github.com/jzheaux\",\n" +
+				"         \"followers_url\":\"https://api.github.com/users/jzheaux/followers\",\n" +
+				"         \"following_url\":\"https://api.github.com/users/jzheaux/following{/other_user}\",\n" +
+				"         \"gists_url\":\"https://api.github.com/users/jzheaux/gists{/gist_id}\",\n" +
+				"         \"starred_url\":\"https://api.github.com/users/jzheaux/starred{/owner}{/repo}\",\n" +
+				"         \"subscriptions_url\":\"https://api.github.com/users/jzheaux/subscriptions\",\n" +
+				"         \"organizations_url\":\"https://api.github.com/users/jzheaux/orgs\",\n" +
+				"         \"repos_url\":\"https://api.github.com/users/jzheaux/repos\",\n" +
+				"         \"events_url\":\"https://api.github.com/users/jzheaux/events{/privacy}\",\n" +
+				"         \"received_events_url\":\"https://api.github.com/users/jzheaux/received_events\",\n" +
+				"         \"type\":\"User\",\n" +
+				"         \"site_admin\":false\n" +
+				"      },\n" +
+				"      \"open_issues\":21,\n" +
+				"      \"closed_issues\":23,\n" +
+				"      \"state\":\"open\",\n" +
+				"      \"created_at\":\"2020-09-16T13:28:03Z\",\n" +
+				"      \"updated_at\":\"2021-04-06T23:47:10Z\",\n" +
+				"      \"due_on\":\"2021-04-12T07:00:00Z\",\n" +
+				"      \"closed_at\":null\n" +
+				"   }\n" +
+				"]";
+		this.server.enqueue(new MockResponse().setBody(responseJson));
+
+		String nextVersion = this.github.getNextReleaseMilestone(this.repositoryRef, "5.5.0-SNAPSHOT");
+
+		RecordedRequest recordedRequest = this.server.takeRequest(1, TimeUnit.SECONDS);
+		assertThat(recordedRequest.getMethod()).isEqualToIgnoringCase("get");
+		assertThat(recordedRequest.getRequestUrl().toString()).isEqualTo(this.baseUrl + "/repos/spring-projects/spring-security/milestones?per_page=100");
+
+		assertThat(nextVersion).isEqualTo("5.5.0");
+	}
+
 }