diff --git a/build.gradle b/build.gradle index 7df5874cd2..47f20b22bd 100644 --- a/build.gradle +++ b/build.gradle @@ -62,6 +62,15 @@ tasks.named("gitHubCheckNextVersionDueToday") { } } +tasks.named("scheduleNextRelease") { + repository { + owner = "spring-projects" + name = "spring-security" + } + weekOfMonth = 3 + dayOfWeek = 1 +} + tasks.named("createGitHubRelease") { repository { owner = "spring-projects" diff --git a/buildSrc/src/main/java/org/springframework/gradle/github/milestones/GitHubMilestoneApi.java b/buildSrc/src/main/java/org/springframework/gradle/github/milestones/GitHubMilestoneApi.java index 96ef15f32b..b9c3739dab 100644 --- a/buildSrc/src/main/java/org/springframework/gradle/github/milestones/GitHubMilestoneApi.java +++ b/buildSrc/src/main/java/org/springframework/gradle/github/milestones/GitHubMilestoneApi.java @@ -18,16 +18,24 @@ package org.springframework.gradle.github.milestones; import java.io.IOException; import java.time.Instant; +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.ZoneId; +import java.time.ZoneOffset; import java.util.List; import java.util.Optional; +import java.util.TimeZone; import java.util.regex.Matcher; import java.util.regex.Pattern; import com.google.common.reflect.TypeToken; import com.google.gson.Gson; +import com.google.gson.GsonBuilder; import okhttp3.Interceptor; +import okhttp3.MediaType; import okhttp3.OkHttpClient; import okhttp3.Request; +import okhttp3.RequestBody; import okhttp3.Response; import org.springframework.gradle.github.RepositoryRef; @@ -37,7 +45,10 @@ public class GitHubMilestoneApi { private OkHttpClient client; - private Gson gson = new Gson(); + private final Gson gson = new GsonBuilder() + .registerTypeAdapter(LocalDate.class, new LocalDateAdapter().nullSafe()) + .registerTypeAdapter(LocalDateTime.class, new LocalDateTimeAdapter().nullSafe()) + .create(); public GitHubMilestoneApi() { this.client = new OkHttpClient.Builder().build(); @@ -54,26 +65,30 @@ public class GitHubMilestoneApi { } public long findMilestoneNumberByTitle(RepositoryRef repositoryRef, String milestoneTitle) { + List milestones = this.getMilestones(repositoryRef); + for (Milestone milestone : milestones) { + if (milestoneTitle.equals(milestone.getTitle())) { + return milestone.getNumber(); + } + } + 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 (only 100 are supported). Could not find open milestone with title " + milestoneTitle + " for repository " + repositoryRef + " Got " + milestones); + } + + public List getMilestones(RepositoryRef repositoryRef) { 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); + throw new RuntimeException("Could not retrieve milestones for repository " + repositoryRef + ". Response " + response); } - List milestones = this.gson.fromJson(response.body().charStream(), new TypeToken>(){}.getType()); - for (Milestone milestone : milestones) { - if (milestoneTitle.equals(milestone.getTitle())) { - return milestone.getNumber(); - } - } - 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); + return this.gson.fromJson(response.body().charStream(), new TypeToken>(){}.getType()); } catch (IOException e) { - throw new RuntimeException("Could not find open milestone with title " + milestoneTitle + " for repository " + repositoryRef, e); + throw new RuntimeException("Could not retrieve milestones for repository " + repositoryRef, e); } } @@ -115,8 +130,8 @@ public class GitHubMilestoneApi { }.getType()); for (Milestone milestone : milestones) { if (milestoneTitle.equals(milestone.getTitle())) { - Instant now = Instant.now(); - return milestone.getDueOn() != null && now.isAfter(milestone.getDueOn().toInstant()); + LocalDate today = LocalDate.now(); + return milestone.getDueOn() != null && today.compareTo(milestone.getDueOn().toLocalDate()) >= 0; } } if (milestones.size() <= 100) { @@ -215,10 +230,28 @@ public class GitHubMilestoneApi { } } -// public boolean isOpenIssuesForMilestoneName(String owner, String repository, String milestoneName) { -// -// } - + /** + * Create a milestone. + * + * @param repository The repository owner/name + * @param milestone The milestone containing a title and due date + */ + public void createMilestone(RepositoryRef repository, Milestone milestone) { + String url = this.baseUrl + "/repos/" + repository.getOwner() + "/" + repository.getName() + "/milestones"; + String json = this.gson.toJson(milestone); + RequestBody body = RequestBody.create(MediaType.parse("application/json"), json); + Request request = new Request.Builder().url(url).post(body).build(); + try { + Response response = this.client.newCall(request).execute(); + if (!response.isSuccessful()) { + throw new RuntimeException(String.format("Could not create milestone %s for repository %s/%s. Got response %s", + milestone.getTitle(), repository.getOwner(), repository.getName(), response)); + } + } catch (IOException ex) { + throw new RuntimeException(String.format("Could not create release %s for repository %s/%s", + milestone.getTitle(), repository.getOwner(), repository.getName()), ex); + } + } private static class AuthorizationInterceptor implements Interceptor { diff --git a/buildSrc/src/main/java/org/springframework/gradle/github/milestones/GitHubMilestonePlugin.java b/buildSrc/src/main/java/org/springframework/gradle/github/milestones/GitHubMilestonePlugin.java index 0137801968..8fc3d51ade 100644 --- a/buildSrc/src/main/java/org/springframework/gradle/github/milestones/GitHubMilestonePlugin.java +++ b/buildSrc/src/main/java/org/springframework/gradle/github/milestones/GitHubMilestonePlugin.java @@ -21,6 +21,8 @@ import org.gradle.api.Plugin; import org.gradle.api.Project; import org.gradle.api.tasks.TaskProvider; +import org.springframework.gradle.github.RepositoryRef; + public class GitHubMilestonePlugin implements Plugin { @Override public void apply(Project project) { @@ -68,5 +70,13 @@ public class GitHubMilestonePlugin implements Plugin { } } }); + project.getTasks().register("scheduleNextRelease", ScheduleNextReleaseTask.class, (scheduleNextRelease) -> { + scheduleNextRelease.doNotTrackState("API call to GitHub needs to check for new milestones every time"); + scheduleNextRelease.setGroup("Release"); + scheduleNextRelease.setDescription("Schedule the next release (even months only) or release train (series of milestones starting in January or July) based on the current version"); + + scheduleNextRelease.setVersion((String) project.findProperty("nextVersion")); + scheduleNextRelease.setGitHubAccessToken((String) project.findProperty("gitHubAccessToken")); + }); } } diff --git a/buildSrc/src/main/java/org/springframework/gradle/github/milestones/LocalDateAdapter.java b/buildSrc/src/main/java/org/springframework/gradle/github/milestones/LocalDateAdapter.java new file mode 100644 index 0000000000..b98e21afb7 --- /dev/null +++ b/buildSrc/src/main/java/org/springframework/gradle/github/milestones/LocalDateAdapter.java @@ -0,0 +1,23 @@ +package org.springframework.gradle.github.milestones; + +import java.io.IOException; +import java.time.LocalDate; + +import com.google.gson.TypeAdapter; +import com.google.gson.stream.JsonReader; +import com.google.gson.stream.JsonWriter; + +/** + * @author Steve Riesenberg + */ +class LocalDateAdapter extends TypeAdapter { + @Override + public void write(JsonWriter jsonWriter, LocalDate localDate) throws IOException { + jsonWriter.value(localDate.toString()); + } + + @Override + public LocalDate read(JsonReader jsonReader) throws IOException { + return LocalDate.parse(jsonReader.nextString()); + } +} diff --git a/buildSrc/src/main/java/org/springframework/gradle/github/milestones/LocalDateTimeAdapter.java b/buildSrc/src/main/java/org/springframework/gradle/github/milestones/LocalDateTimeAdapter.java new file mode 100644 index 0000000000..875658748f --- /dev/null +++ b/buildSrc/src/main/java/org/springframework/gradle/github/milestones/LocalDateTimeAdapter.java @@ -0,0 +1,25 @@ +package org.springframework.gradle.github.milestones; + +import java.io.IOException; +import java.time.LocalDateTime; +import java.time.ZoneOffset; +import java.time.format.DateTimeFormatter; + +import com.google.gson.TypeAdapter; +import com.google.gson.stream.JsonReader; +import com.google.gson.stream.JsonWriter; + +/** + * @author Steve Riesenberg + */ +class LocalDateTimeAdapter extends TypeAdapter { + @Override + public void write(JsonWriter jsonWriter, LocalDateTime localDateTime) throws IOException { + jsonWriter.value(localDateTime.atOffset(ZoneOffset.UTC).format(DateTimeFormatter.ISO_ZONED_DATE_TIME)); + } + + @Override + public LocalDateTime read(JsonReader jsonReader) throws IOException { + return LocalDateTime.parse(jsonReader.nextString(), DateTimeFormatter.ISO_ZONED_DATE_TIME); + } +} diff --git a/buildSrc/src/main/java/org/springframework/gradle/github/milestones/Milestone.java b/buildSrc/src/main/java/org/springframework/gradle/github/milestones/Milestone.java index 08ce6c1bab..09330ee851 100644 --- a/buildSrc/src/main/java/org/springframework/gradle/github/milestones/Milestone.java +++ b/buildSrc/src/main/java/org/springframework/gradle/github/milestones/Milestone.java @@ -18,15 +18,19 @@ package org.springframework.gradle.github.milestones; import com.google.gson.annotations.SerializedName; +import java.time.LocalDateTime; import java.util.Date; +/** + * @author Steve Riesenberg + */ public class Milestone { private String title; - private long number; + private Long number; @SerializedName("due_on") - private Date dueOn; + private LocalDateTime dueOn; public String getTitle() { return title; @@ -36,19 +40,19 @@ public class Milestone { this.title = title; } - public long getNumber() { + public Long getNumber() { return number; } - public void setNumber(long number) { + public void setNumber(Long number) { this.number = number; } - public Date getDueOn() { + public LocalDateTime getDueOn() { return dueOn; } - public void setDueOn(Date dueOn) { + public void setDueOn(LocalDateTime dueOn) { this.dueOn = dueOn; } @@ -57,7 +61,7 @@ public class Milestone { return "Milestone{" + "title='" + title + '\'' + ", number='" + number + '\'' + - ", dueOn='" + dueOn + + ", dueOn='" + dueOn + '\'' + '}'; } } diff --git a/buildSrc/src/main/java/org/springframework/gradle/github/milestones/ScheduleNextReleaseTask.java b/buildSrc/src/main/java/org/springframework/gradle/github/milestones/ScheduleNextReleaseTask.java new file mode 100644 index 0000000000..4b9527b315 --- /dev/null +++ b/buildSrc/src/main/java/org/springframework/gradle/github/milestones/ScheduleNextReleaseTask.java @@ -0,0 +1,142 @@ +/* + * Copyright 2002-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 java.time.LocalDate; +import java.time.LocalTime; + +import org.gradle.api.DefaultTask; +import org.gradle.api.tasks.Input; +import org.gradle.api.tasks.TaskAction; + +import org.springframework.gradle.github.RepositoryRef; + +/** + * @author Steve Riesenberg + */ +public class ScheduleNextReleaseTask extends DefaultTask { + @Input + private RepositoryRef repository = new RepositoryRef(); + + @Input + private String gitHubAccessToken; + + @Input + private String version; + + @Input + private Integer weekOfMonth; + + @Input + private Integer dayOfWeek; + + @TaskAction + public void scheduleNextRelease() { + GitHubMilestoneApi gitHubMilestoneApi = new GitHubMilestoneApi(this.gitHubAccessToken); + String nextReleaseMilestone = gitHubMilestoneApi.getNextReleaseMilestone(this.repository, this.version); + + // If the next release contains a dash (e.g. 5.6.0-RC1), it is already scheduled + if (nextReleaseMilestone.contains("-")) { + return; + } + + // Check to see if a scheduled GA version already exists + boolean hasExistingMilestone = gitHubMilestoneApi.getMilestones(this.repository).stream() + .anyMatch(milestone -> nextReleaseMilestone.equals(milestone.getTitle())); + if (hasExistingMilestone) { + return; + } + + // Next milestone is either a patch version or minor version + // Note: Major versions will be handled like minor and get a release + // train which can be manually updated to match the desired schedule. + if (nextReleaseMilestone.endsWith(".0")) { + // Create M1, M2, M3, RC1 and GA milestones for release train + getReleaseTrain(nextReleaseMilestone).getTrainDates().forEach((milestoneTitle, dueOn) -> { + Milestone milestone = new Milestone(); + milestone.setTitle(milestoneTitle); + // Note: GitHub seems to store full date/time as UTC then displays + // as a date (no time) in your timezone, which means the date will + // not always be the same date as we intend. + // Using 12pm/noon UTC allows GitHub to schedule and display the + // correct date. + milestone.setDueOn(dueOn.atTime(LocalTime.NOON)); + gitHubMilestoneApi.createMilestone(this.repository, milestone); + }); + } else { + // Create GA milestone for patch release on the next even month + LocalDate startDate = LocalDate.now(); + LocalDate dueOn = getReleaseTrain(nextReleaseMilestone).getNextReleaseDate(startDate); + Milestone milestone = new Milestone(); + milestone.setTitle(nextReleaseMilestone); + milestone.setDueOn(dueOn.atTime(LocalTime.NOON)); + gitHubMilestoneApi.createMilestone(this.repository, milestone); + } + } + + private SpringReleaseTrain getReleaseTrain(String nextReleaseMilestone) { + SpringReleaseTrainSpec releaseTrainSpec = + SpringReleaseTrainSpec.builder() + .nextTrain() + .version(nextReleaseMilestone) + .weekOfMonth(this.weekOfMonth) + .dayOfWeek(this.dayOfWeek) + .build(); + + return new SpringReleaseTrain(releaseTrainSpec); + } + + public RepositoryRef getRepository() { + return this.repository; + } + + public void setRepository(RepositoryRef repository) { + this.repository = repository; + } + + public String getGitHubAccessToken() { + return this.gitHubAccessToken; + } + + public void setGitHubAccessToken(String gitHubAccessToken) { + this.gitHubAccessToken = gitHubAccessToken; + } + + public String getVersion() { + return this.version; + } + + public void setVersion(String version) { + this.version = version; + } + + public Integer getWeekOfMonth() { + return weekOfMonth; + } + + public void setWeekOfMonth(Integer weekOfMonth) { + this.weekOfMonth = weekOfMonth; + } + + public Integer getDayOfWeek() { + return dayOfWeek; + } + + public void setDayOfWeek(Integer dayOfWeek) { + this.dayOfWeek = dayOfWeek; + } +} diff --git a/buildSrc/src/main/java/org/springframework/gradle/github/milestones/SpringReleaseTrain.java b/buildSrc/src/main/java/org/springframework/gradle/github/milestones/SpringReleaseTrain.java new file mode 100644 index 0000000000..cce1c0bc85 --- /dev/null +++ b/buildSrc/src/main/java/org/springframework/gradle/github/milestones/SpringReleaseTrain.java @@ -0,0 +1,134 @@ +/* + * Copyright 2002-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 java.time.DayOfWeek; +import java.time.LocalDate; +import java.time.Month; +import java.time.Year; +import java.util.LinkedHashMap; +import java.util.Map; + +/** + * Spring release train generator based on rules contained in a specification. + *

+ * The rules are: + *

    + *
  1. Train 1 (January-May) or 2 (July-November)
  2. + *
  3. Version number (e.g. 0.1.2, 1.0.0, etc.)
  4. + *
  5. Week of month (1st, 2nd, 3rd, 4th)
  6. + *
  7. Day of week (Monday-Friday)
  8. + *
  9. Year (e.g. 2020, 2021, etc.)
  10. + *
+ * + * The release train generated will contain M1, M2, M3, RC1 and GA versions + * mapped to their respective dates in the train. + * + * @author Steve Riesenberg + */ +public final class SpringReleaseTrain { + private final SpringReleaseTrainSpec releaseTrainSpec; + + public SpringReleaseTrain(SpringReleaseTrainSpec releaseTrainSpec) { + this.releaseTrainSpec = releaseTrainSpec; + } + + /** + * Calculate release train dates based on the release train specification. + * + * @return A mapping of release milestones to scheduled release dates + */ + public Map getTrainDates() { + Map releaseDates = new LinkedHashMap<>(); + switch (this.releaseTrainSpec.getTrain()) { + case ONE: + addTrainDate(releaseDates, "M1", Month.JANUARY); + addTrainDate(releaseDates, "M2", Month.FEBRUARY); + addTrainDate(releaseDates, "M3", Month.MARCH); + addTrainDate(releaseDates, "RC1", Month.APRIL); + addTrainDate(releaseDates, null, Month.MAY); + break; + case TWO: + addTrainDate(releaseDates, "M1", Month.JULY); + addTrainDate(releaseDates, "M2", Month.AUGUST); + addTrainDate(releaseDates, "M3", Month.SEPTEMBER); + addTrainDate(releaseDates, "RC1", Month.OCTOBER); + addTrainDate(releaseDates, null, Month.NOVEMBER); + break; + } + + return releaseDates; + } + + /** + * Determine if a given date matches the due date of given version. + * + * @param version The version number (e.g. 5.6.0-M1, 5.6.0, etc.) + * @param expectedDate The expected date + * @return true if the given date matches the due date of the given version, false otherwise + */ + public boolean isTrainDate(String version, LocalDate expectedDate) { + return expectedDate.isEqual(getTrainDates().get(version)); + } + + /** + * Calculate the next release date following the given date. + *

+ * The next release date is always on an even month so that a patch release + * is the month after the GA version of a release train. This method does + * not consider the year of the release train, only the given start date. + * + * @param startDate The start date + * @return The next release date following the given date + */ + public LocalDate getNextReleaseDate(LocalDate startDate) { + LocalDate trainDate; + LocalDate currentDate = startDate; + do { + trainDate = calculateReleaseDate( + Year.of(currentDate.getYear()), + currentDate.getMonth(), + this.releaseTrainSpec.getDayOfWeek().getDayOfWeek(), + this.releaseTrainSpec.getWeekOfMonth().getDayOffset() + ); + currentDate = currentDate.plusMonths(1); + } while (!trainDate.isAfter(startDate) || trainDate.getMonthValue() % 2 != 0); + + return trainDate; + } + + private void addTrainDate(Map releaseDates, String milestone, Month month) { + LocalDate releaseDate = calculateReleaseDate( + this.releaseTrainSpec.getYear(), + month, + this.releaseTrainSpec.getDayOfWeek().getDayOfWeek(), + this.releaseTrainSpec.getWeekOfMonth().getDayOffset() + ); + String suffix = (milestone == null) ? "" : "-" + milestone; + releaseDates.put(this.releaseTrainSpec.getVersion() + suffix, releaseDate); + } + + private static LocalDate calculateReleaseDate(Year year, Month month, DayOfWeek dayOfWeek, int dayOffset) { + LocalDate firstDayOfMonth = year.atMonth(month).atDay(1); + int dayOfWeekOffset = dayOfWeek.getValue() - firstDayOfMonth.getDayOfWeek().getValue(); + if (dayOfWeekOffset < 0) { + dayOfWeekOffset += 7; + } + + return firstDayOfMonth.plusDays(dayOfWeekOffset).plusDays(dayOffset); + } +} diff --git a/buildSrc/src/main/java/org/springframework/gradle/github/milestones/SpringReleaseTrainSpec.java b/buildSrc/src/main/java/org/springframework/gradle/github/milestones/SpringReleaseTrainSpec.java new file mode 100644 index 0000000000..792e390c00 --- /dev/null +++ b/buildSrc/src/main/java/org/springframework/gradle/github/milestones/SpringReleaseTrainSpec.java @@ -0,0 +1,205 @@ +/* + * Copyright 2002-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 java.time.LocalDate; +import java.time.Month; +import java.time.Year; + +import org.springframework.util.Assert; + +/** + * A specification for a release train. + * + * @author Steve Riesenberg + * @see SpringReleaseTrain + */ +public final class SpringReleaseTrainSpec { + private final Train train; + private final String version; + private final WeekOfMonth weekOfMonth; + private final DayOfWeek dayOfWeek; + private final Year year; + + public SpringReleaseTrainSpec(Train train, String version, WeekOfMonth weekOfMonth, DayOfWeek dayOfWeek, Year year) { + this.train = train; + this.version = version; + this.weekOfMonth = weekOfMonth; + this.dayOfWeek = dayOfWeek; + this.year = year; + } + + public Train getTrain() { + return train; + } + + public String getVersion() { + return version; + } + + public WeekOfMonth getWeekOfMonth() { + return weekOfMonth; + } + + public DayOfWeek getDayOfWeek() { + return dayOfWeek; + } + + public Year getYear() { + return year; + } + + public static Builder builder() { + return new Builder(); + } + + public enum WeekOfMonth { + FIRST(0), SECOND(7), THIRD(14), FOURTH(21); + + private final int dayOffset; + + WeekOfMonth(int dayOffset) { + this.dayOffset = dayOffset; + } + + public int getDayOffset() { + return dayOffset; + } + } + + public enum DayOfWeek { + MONDAY(java.time.DayOfWeek.MONDAY), + TUESDAY(java.time.DayOfWeek.TUESDAY), + WEDNESDAY(java.time.DayOfWeek.WEDNESDAY), + THURSDAY(java.time.DayOfWeek.THURSDAY), + FRIDAY(java.time.DayOfWeek.FRIDAY); + + private final java.time.DayOfWeek dayOfWeek; + + DayOfWeek(java.time.DayOfWeek dayOfWeek) { + this.dayOfWeek = dayOfWeek; + } + + public java.time.DayOfWeek getDayOfWeek() { + return dayOfWeek; + } + } + + public enum Train { + ONE, TWO + } + + public static class Builder { + private Train train; + private String version; + private WeekOfMonth weekOfMonth; + private DayOfWeek dayOfWeek; + private Year year; + + private Builder() { + } + + public Builder train(int train) { + switch (train) { + case 1: this.train = Train.ONE; break; + case 2: this.train = Train.TWO; break; + default: throw new IllegalArgumentException("Invalid train: " + train); + } + return this; + } + + public Builder train(Train train) { + this.train = train; + return this; + } + + public Builder nextTrain() { + // Search for next train starting with this month + return nextTrain(LocalDate.now().withDayOfMonth(1)); + } + + public Builder nextTrain(LocalDate startDate) { + Train nextTrain = null; + + // Search for next train from a given start date + LocalDate currentDate = startDate; + while (nextTrain == null) { + if (currentDate.getMonth() == Month.JANUARY) { + nextTrain = Train.ONE; + } else if (currentDate.getMonth() == Month.JULY) { + nextTrain = Train.TWO; + } + + currentDate = currentDate.plusMonths(1); + } + + return train(nextTrain).year(currentDate.getYear()); + } + + public Builder version(String version) { + this.version = version; + return this; + } + + public Builder weekOfMonth(int weekOfMonth) { + switch (weekOfMonth) { + case 1: this.weekOfMonth = WeekOfMonth.FIRST; break; + case 2: this.weekOfMonth = WeekOfMonth.SECOND; break; + case 3: this.weekOfMonth = WeekOfMonth.THIRD; break; + case 4: this.weekOfMonth = WeekOfMonth.FOURTH; break; + default: throw new IllegalArgumentException("Invalid weekOfMonth: " + weekOfMonth); + } + return this; + } + + public Builder weekOfMonth(WeekOfMonth weekOfMonth) { + this.weekOfMonth = weekOfMonth; + return this; + } + + public Builder dayOfWeek(int dayOfWeek) { + switch (dayOfWeek) { + case 1: this.dayOfWeek = DayOfWeek.MONDAY; break; + case 2: this.dayOfWeek = DayOfWeek.TUESDAY; break; + case 3: this.dayOfWeek = DayOfWeek.WEDNESDAY; break; + case 4: this.dayOfWeek = DayOfWeek.THURSDAY; break; + case 5: this.dayOfWeek = DayOfWeek.FRIDAY; break; + default: throw new IllegalArgumentException("Invalid dayOfWeek: " + dayOfWeek); + } + return this; + } + + public Builder dayOfWeek(DayOfWeek dayOfWeek) { + this.dayOfWeek = dayOfWeek; + return this; + } + + public Builder year(int year) { + this.year = Year.of(year); + return this; + } + + public SpringReleaseTrainSpec build() { + Assert.notNull(train, "train cannot be null"); + Assert.notNull(version, "version cannot be null"); + Assert.notNull(weekOfMonth, "weekOfMonth cannot be null"); + Assert.notNull(dayOfWeek, "dayOfWeek cannot be null"); + Assert.notNull(year, "year cannot be null"); + return new SpringReleaseTrainSpec(train, version, weekOfMonth, dayOfWeek, year); + } + } +} diff --git a/buildSrc/src/test/java/io/spring/gradle/github/milestones/GitHubMilestoneApiTests.java b/buildSrc/src/test/java/io/spring/gradle/github/milestones/GitHubMilestoneApiTests.java deleted file mode 100644 index b9b0764ee5..0000000000 --- a/buildSrc/src/test/java/io/spring/gradle/github/milestones/GitHubMilestoneApiTests.java +++ /dev/null @@ -1,389 +0,0 @@ -package io.spring.gradle.github.milestones; - -import java.util.concurrent.TimeUnit; - -import okhttp3.mockwebserver.MockResponse; -import okhttp3.mockwebserver.MockWebServer; -import okhttp3.mockwebserver.RecordedRequest; -import org.junit.jupiter.api.AfterEach; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; - -import org.springframework.gradle.github.RepositoryRef; -import org.springframework.gradle.github.milestones.GitHubMilestoneApi; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatExceptionOfType; - - -public class GitHubMilestoneApiTests { - private GitHubMilestoneApi github; - - private RepositoryRef repositoryRef = RepositoryRef.owner("spring-projects").repository("spring-security").build(); - - private MockWebServer server; - - private String baseUrl; - - @BeforeEach - public void setup() throws Exception { - this.server = new MockWebServer(); - this.server.start(); - this.github = new GitHubMilestoneApi("mock-oauth-token"); - this.baseUrl = this.server.url("/api").toString(); - this.github.setBaseUrl(this.baseUrl); - } - - @AfterEach - public void cleanup() throws Exception { - this.server.shutdown(); - } - - @Test - public void findMilestoneNumberByTitleWhenFoundThenSuccess() 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)); - - long milestoneNumberByTitle = this.github.findMilestoneNumberByTitle(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(milestoneNumberByTitle).isEqualTo(191); - } - - @Test - public void findMilestoneNumberByTitleWhenNotFoundThenException() 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.findMilestoneNumberByTitle(this.repositoryRef, "missing")); - } - - @Test - public void isOpenIssuesForMilestoneNumberWhenAllClosedThenFalse() throws Exception { - String responseJson = "[]"; - long milestoneNumber = 202; - this.server.enqueue(new MockResponse().setBody(responseJson)); - - assertThat(this.github.isOpenIssuesForMilestoneNumber(this.repositoryRef, milestoneNumber)).isFalse(); - - 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/issues?per_page=1&milestone=" + milestoneNumber); - } - - @Test - public void isOpenIssuesForMilestoneNumberWhenOpenIssuesThenTrue() throws Exception { - String responseJson = "[\n" + - " {\n" + - " \"url\":\"https://api.github.com/repos/spring-projects/spring-security/issues/9562\",\n" + - " \"repository_url\":\"https://api.github.com/repos/spring-projects/spring-security\",\n" + - " \"labels_url\":\"https://api.github.com/repos/spring-projects/spring-security/issues/9562/labels{/name}\",\n" + - " \"comments_url\":\"https://api.github.com/repos/spring-projects/spring-security/issues/9562/comments\",\n" + - " \"events_url\":\"https://api.github.com/repos/spring-projects/spring-security/issues/9562/events\",\n" + - " \"html_url\":\"https://github.com/spring-projects/spring-security/pull/9562\",\n" + - " \"id\":851886504,\n" + - " \"node_id\":\"MDExOlB1bGxSZXF1ZXN0NjEwMjMzMDcw\",\n" + - " \"number\":9562,\n" + - " \"title\":\"Add package-list\",\n" + - " \"user\":{\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" + - " \"labels\":[\n" + - " {\n" + - " \"id\":322225043,\n" + - " \"node_id\":\"MDU6TGFiZWwzMjIyMjUwNDM=\",\n" + - " \"url\":\"https://api.github.com/repos/spring-projects/spring-security/labels/in:%20build\",\n" + - " \"name\":\"in: build\",\n" + - " \"color\":\"e8f9de\",\n" + - " \"default\":false,\n" + - " \"description\":\"An issue in the build\"\n" + - " },\n" + - " {\n" + - " \"id\":322225079,\n" + - " \"node_id\":\"MDU6TGFiZWwzMjIyMjUwNzk=\",\n" + - " \"url\":\"https://api.github.com/repos/spring-projects/spring-security/labels/type:%20bug\",\n" + - " \"name\":\"type: bug\",\n" + - " \"color\":\"e3d9fc\",\n" + - " \"default\":false,\n" + - " \"description\":\"A general bug\"\n" + - " }\n" + - " ],\n" + - " \"state\":\"open\",\n" + - " \"locked\":false,\n" + - " \"assignee\":{\n" + - " \"login\":\"rwinch\",\n" + - " \"id\":362503,\n" + - " \"node_id\":\"MDQ6VXNlcjM2MjUwMw==\",\n" + - " \"avatar_url\":\"https://avatars.githubusercontent.com/u/362503?v=4\",\n" + - " \"gravatar_id\":\"\",\n" + - " \"url\":\"https://api.github.com/users/rwinch\",\n" + - " \"html_url\":\"https://github.com/rwinch\",\n" + - " \"followers_url\":\"https://api.github.com/users/rwinch/followers\",\n" + - " \"following_url\":\"https://api.github.com/users/rwinch/following{/other_user}\",\n" + - " \"gists_url\":\"https://api.github.com/users/rwinch/gists{/gist_id}\",\n" + - " \"starred_url\":\"https://api.github.com/users/rwinch/starred{/owner}{/repo}\",\n" + - " \"subscriptions_url\":\"https://api.github.com/users/rwinch/subscriptions\",\n" + - " \"organizations_url\":\"https://api.github.com/users/rwinch/orgs\",\n" + - " \"repos_url\":\"https://api.github.com/users/rwinch/repos\",\n" + - " \"events_url\":\"https://api.github.com/users/rwinch/events{/privacy}\",\n" + - " \"received_events_url\":\"https://api.github.com/users/rwinch/received_events\",\n" + - " \"type\":\"User\",\n" + - " \"site_admin\":false\n" + - " },\n" + - " \"assignees\":[\n" + - " {\n" + - " \"login\":\"rwinch\",\n" + - " \"id\":362503,\n" + - " \"node_id\":\"MDQ6VXNlcjM2MjUwMw==\",\n" + - " \"avatar_url\":\"https://avatars.githubusercontent.com/u/362503?v=4\",\n" + - " \"gravatar_id\":\"\",\n" + - " \"url\":\"https://api.github.com/users/rwinch\",\n" + - " \"html_url\":\"https://github.com/rwinch\",\n" + - " \"followers_url\":\"https://api.github.com/users/rwinch/followers\",\n" + - " \"following_url\":\"https://api.github.com/users/rwinch/following{/other_user}\",\n" + - " \"gists_url\":\"https://api.github.com/users/rwinch/gists{/gist_id}\",\n" + - " \"starred_url\":\"https://api.github.com/users/rwinch/starred{/owner}{/repo}\",\n" + - " \"subscriptions_url\":\"https://api.github.com/users/rwinch/subscriptions\",\n" + - " \"organizations_url\":\"https://api.github.com/users/rwinch/orgs\",\n" + - " \"repos_url\":\"https://api.github.com/users/rwinch/repos\",\n" + - " \"events_url\":\"https://api.github.com/users/rwinch/events{/privacy}\",\n" + - " \"received_events_url\":\"https://api.github.com/users/rwinch/received_events\",\n" + - " \"type\":\"User\",\n" + - " \"site_admin\":false\n" + - " }\n" + - " ],\n" + - " \"milestone\":{\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" + - " \"comments\":0,\n" + - " \"created_at\":\"2021-04-06T23:47:10Z\",\n" + - " \"updated_at\":\"2021-04-07T17:00:00Z\",\n" + - " \"closed_at\":null,\n" + - " \"author_association\":\"MEMBER\",\n" + - " \"active_lock_reason\":null,\n" + - " \"pull_request\":{\n" + - " \"url\":\"https://api.github.com/repos/spring-projects/spring-security/pulls/9562\",\n" + - " \"html_url\":\"https://github.com/spring-projects/spring-security/pull/9562\",\n" + - " \"diff_url\":\"https://github.com/spring-projects/spring-security/pull/9562.diff\",\n" + - " \"patch_url\":\"https://github.com/spring-projects/spring-security/pull/9562.patch\"\n" + - " },\n" + - " \"body\":\"Closes gh-9528\\r\\n\\r\\n\\r\\n\\r\\n\\r\\n\",\n" + - " \"performed_via_github_app\":null\n" + - " }\n" + - "]"; - long milestoneNumber = 191; - this.server.enqueue(new MockResponse().setBody(responseJson)); - - assertThat(this.github.isOpenIssuesForMilestoneNumber(this.repositoryRef, milestoneNumber)).isTrue(); - - 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/issues?per_page=1&milestone=" + milestoneNumber); - } - -} diff --git a/buildSrc/src/test/java/org/springframework/gradle/github/milestones/GitHubMilestoneApiTests.java b/buildSrc/src/test/java/org/springframework/gradle/github/milestones/GitHubMilestoneApiTests.java index 45bfb6b58a..575a3858c7 100644 --- a/buildSrc/src/test/java/org/springframework/gradle/github/milestones/GitHubMilestoneApiTests.java +++ b/buildSrc/src/test/java/org/springframework/gradle/github/milestones/GitHubMilestoneApiTests.java @@ -1,6 +1,10 @@ package org.springframework.gradle.github.milestones; +import java.nio.charset.Charset; import java.time.Instant; +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.LocalTime; import java.util.concurrent.TimeUnit; import okhttp3.mockwebserver.MockResponse; @@ -1195,4 +1199,27 @@ public class GitHubMilestoneApiTests { assertThat(nextVersion).isEqualTo("5.5.0"); } + @Test + public void createMilestoneWhenValidParametersThenSuccess() throws Exception { + this.server.enqueue(new MockResponse().setResponseCode(204)); + Milestone milestone = new Milestone(); + milestone.setTitle("1.0.0"); + milestone.setDueOn(LocalDate.of(2022, 5, 4).atTime(LocalTime.NOON)); + this.github.createMilestone(this.repositoryRef, milestone); + + RecordedRequest recordedRequest = this.server.takeRequest(1, TimeUnit.SECONDS); + assertThat(recordedRequest.getMethod()).isEqualToIgnoringCase("post"); + assertThat(recordedRequest.getRequestUrl().toString()) + .isEqualTo(this.baseUrl + "/repos/spring-projects/spring-security/milestones"); + assertThat(recordedRequest.getBody().readString(Charset.defaultCharset())) + .isEqualTo("{\"title\":\"1.0.0\",\"due_on\":\"2022-05-04T12:00:00Z\"}"); + } + + @Test + public void createMilestoneWhenErrorResponseThenException() throws Exception { + this.server.enqueue(new MockResponse().setResponseCode(400)); + assertThatExceptionOfType(RuntimeException.class) + .isThrownBy(() -> this.github.createMilestone(this.repositoryRef, new Milestone())); + } + } diff --git a/buildSrc/src/test/java/org/springframework/gradle/github/milestones/SpringReleaseTrainTests.java b/buildSrc/src/test/java/org/springframework/gradle/github/milestones/SpringReleaseTrainTests.java new file mode 100644 index 0000000000..4721aca6ce --- /dev/null +++ b/buildSrc/src/test/java/org/springframework/gradle/github/milestones/SpringReleaseTrainTests.java @@ -0,0 +1,245 @@ +/* + * Copyright 2002-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 java.time.LocalDate; +import java.time.Year; +import java.util.Map; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvSource; + +import org.springframework.gradle.github.milestones.SpringReleaseTrainSpec.Train; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * @author Steve Riesenberg + */ +public class SpringReleaseTrainTests { + @ParameterizedTest + @CsvSource({ + "2019-12-31, ONE, 2020", + "2020-01-01, ONE, 2020", + "2020-01-31, ONE, 2020", + "2020-02-01, TWO, 2020", + "2020-07-31, TWO, 2020", + "2020-08-01, ONE, 2021" + }) + public void nextTrainWhenBoundaryConditionsThenSuccess(LocalDate startDate, Train expectedTrain, Year expectedYear) { + SpringReleaseTrainSpec releaseTrainSpec = + SpringReleaseTrainSpec.builder() + .nextTrain(startDate) + .version("1.0.0") + .weekOfMonth(2) + .dayOfWeek(2) + .build(); + assertThat(releaseTrainSpec.getTrain()).isEqualTo(expectedTrain); + assertThat(releaseTrainSpec.getYear()).isEqualTo(expectedYear); + } + + @Test + public void getTrainDatesWhenTrainOneIsSecondTuesdayOf2020ThenSuccess() { + SpringReleaseTrainSpec releaseTrainSpec = + SpringReleaseTrainSpec.builder() + .train(1) + .version("1.0.0") + .weekOfMonth(2) + .dayOfWeek(2) + .year(2020) + .build(); + + SpringReleaseTrain releaseTrain = new SpringReleaseTrain(releaseTrainSpec); + Map trainDates = releaseTrain.getTrainDates(); + assertThat(trainDates).hasSize(5); + assertThat(trainDates.get("1.0.0-M1")).isEqualTo(LocalDate.of(2020, 1, 14)); + assertThat(trainDates.get("1.0.0-M2")).isEqualTo(LocalDate.of(2020, 2, 11)); + assertThat(trainDates.get("1.0.0-M3")).isEqualTo(LocalDate.of(2020, 3, 10)); + assertThat(trainDates.get("1.0.0-RC1")).isEqualTo(LocalDate.of(2020, 4, 14)); + assertThat(trainDates.get("1.0.0")).isEqualTo(LocalDate.of(2020, 5, 12)); + } + + @Test + public void getTrainDatesWhenTrainTwoIsSecondTuesdayOf2020ThenSuccess() { + SpringReleaseTrainSpec releaseTrainSpec = + SpringReleaseTrainSpec.builder() + .train(2) + .version("1.0.0") + .weekOfMonth(2) + .dayOfWeek(2) + .year(2020) + .build(); + + SpringReleaseTrain releaseTrain = new SpringReleaseTrain(releaseTrainSpec); + Map trainDates = releaseTrain.getTrainDates(); + assertThat(trainDates).hasSize(5); + assertThat(trainDates.get("1.0.0-M1")).isEqualTo(LocalDate.of(2020, 7, 14)); + assertThat(trainDates.get("1.0.0-M2")).isEqualTo(LocalDate.of(2020, 8, 11)); + assertThat(trainDates.get("1.0.0-M3")).isEqualTo(LocalDate.of(2020, 9, 8)); + assertThat(trainDates.get("1.0.0-RC1")).isEqualTo(LocalDate.of(2020, 10, 13)); + assertThat(trainDates.get("1.0.0")).isEqualTo(LocalDate.of(2020, 11, 10)); + } + + @Test + public void getTrainDatesWhenTrainOneIsSecondTuesdayOf2022ThenSuccess() { + SpringReleaseTrainSpec releaseTrainSpec = + SpringReleaseTrainSpec.builder() + .train(1) + .version("1.0.0") + .weekOfMonth(2) + .dayOfWeek(2) + .year(2022) + .build(); + + SpringReleaseTrain releaseTrain = new SpringReleaseTrain(releaseTrainSpec); + Map trainDates = releaseTrain.getTrainDates(); + assertThat(trainDates).hasSize(5); + assertThat(trainDates.get("1.0.0-M1")).isEqualTo(LocalDate.of(2022, 1, 11)); + assertThat(trainDates.get("1.0.0-M2")).isEqualTo(LocalDate.of(2022, 2, 8)); + assertThat(trainDates.get("1.0.0-M3")).isEqualTo(LocalDate.of(2022, 3, 8)); + assertThat(trainDates.get("1.0.0-RC1")).isEqualTo(LocalDate.of(2022, 4, 12)); + assertThat(trainDates.get("1.0.0")).isEqualTo(LocalDate.of(2022, 5, 10)); + } + + @Test + public void getTrainDatesWhenTrainTwoIsSecondTuesdayOf2022ThenSuccess() { + SpringReleaseTrainSpec releaseTrainSpec = + SpringReleaseTrainSpec.builder() + .train(2) + .version("1.0.0") + .weekOfMonth(2) + .dayOfWeek(2) + .year(2022) + .build(); + + SpringReleaseTrain releaseTrain = new SpringReleaseTrain(releaseTrainSpec); + Map trainDates = releaseTrain.getTrainDates(); + assertThat(trainDates).hasSize(5); + assertThat(trainDates.get("1.0.0-M1")).isEqualTo(LocalDate.of(2022, 7, 12)); + assertThat(trainDates.get("1.0.0-M2")).isEqualTo(LocalDate.of(2022, 8, 9)); + assertThat(trainDates.get("1.0.0-M3")).isEqualTo(LocalDate.of(2022, 9, 13)); + assertThat(trainDates.get("1.0.0-RC1")).isEqualTo(LocalDate.of(2022, 10, 11)); + assertThat(trainDates.get("1.0.0")).isEqualTo(LocalDate.of(2022, 11, 8)); + } + + @Test + public void getTrainDatesWhenTrainOneIsThirdMondayOf2022ThenSuccess() { + SpringReleaseTrainSpec releaseTrainSpec = + SpringReleaseTrainSpec.builder() + .train(1) + .version("1.0.0") + .weekOfMonth(3) + .dayOfWeek(1) + .year(2022) + .build(); + + SpringReleaseTrain releaseTrain = new SpringReleaseTrain(releaseTrainSpec); + Map trainDates = releaseTrain.getTrainDates(); + assertThat(trainDates).hasSize(5); + assertThat(trainDates.get("1.0.0-M1")).isEqualTo(LocalDate.of(2022, 1, 17)); + assertThat(trainDates.get("1.0.0-M2")).isEqualTo(LocalDate.of(2022, 2, 21)); + assertThat(trainDates.get("1.0.0-M3")).isEqualTo(LocalDate.of(2022, 3, 21)); + assertThat(trainDates.get("1.0.0-RC1")).isEqualTo(LocalDate.of(2022, 4, 18)); + assertThat(trainDates.get("1.0.0")).isEqualTo(LocalDate.of(2022, 5, 16)); + } + + @Test + public void getTrainDatesWhenTrainTwoIsThirdMondayOf2022ThenSuccess() { + SpringReleaseTrainSpec releaseTrainSpec = + SpringReleaseTrainSpec.builder() + .train(2) + .version("1.0.0") + .weekOfMonth(3) + .dayOfWeek(1) + .year(2022) + .build(); + + SpringReleaseTrain releaseTrain = new SpringReleaseTrain(releaseTrainSpec); + Map trainDates = releaseTrain.getTrainDates(); + assertThat(trainDates).hasSize(5); + assertThat(trainDates.get("1.0.0-M1")).isEqualTo(LocalDate.of(2022, 7, 18)); + assertThat(trainDates.get("1.0.0-M2")).isEqualTo(LocalDate.of(2022, 8, 15)); + assertThat(trainDates.get("1.0.0-M3")).isEqualTo(LocalDate.of(2022, 9, 19)); + assertThat(trainDates.get("1.0.0-RC1")).isEqualTo(LocalDate.of(2022, 10, 17)); + assertThat(trainDates.get("1.0.0")).isEqualTo(LocalDate.of(2022, 11, 21)); + } + + @Test + public void isTrainDateWhenTrainOneIsThirdMondayOf2022ThenSuccess() { + SpringReleaseTrainSpec releaseTrainSpec = + SpringReleaseTrainSpec.builder() + .train(1) + .version("1.0.0") + .weekOfMonth(3) + .dayOfWeek(1) + .year(2022) + .build(); + + SpringReleaseTrain releaseTrain = new SpringReleaseTrain(releaseTrainSpec); + for (int dayOfMonth = 1; dayOfMonth <= 31; dayOfMonth++) { + assertThat(releaseTrain.isTrainDate("1.0.0-M1", LocalDate.of(2022, 1, dayOfMonth))).isEqualTo(dayOfMonth == 17); + } + for (int dayOfMonth = 1; dayOfMonth <= 28; dayOfMonth++) { + assertThat(releaseTrain.isTrainDate("1.0.0-M2", LocalDate.of(2022, 2, dayOfMonth))).isEqualTo(dayOfMonth == 21); + } + for (int dayOfMonth = 1; dayOfMonth <= 31; dayOfMonth++) { + assertThat(releaseTrain.isTrainDate("1.0.0-M3", LocalDate.of(2022, 3, dayOfMonth))).isEqualTo(dayOfMonth == 21); + } + for (int dayOfMonth = 1; dayOfMonth <= 30; dayOfMonth++) { + assertThat(releaseTrain.isTrainDate("1.0.0-RC1", LocalDate.of(2022, 4, dayOfMonth))).isEqualTo(dayOfMonth == 18); + } + for (int dayOfMonth = 1; dayOfMonth <= 31; dayOfMonth++) { + assertThat(releaseTrain.isTrainDate("1.0.0", LocalDate.of(2022, 5, dayOfMonth))).isEqualTo(dayOfMonth == 16); + } + } + + @ParameterizedTest + @CsvSource({ + "2022-01-01, 2022-02-21", + "2022-02-01, 2022-02-21", + "2022-02-21, 2022-04-18", + "2022-03-01, 2022-04-18", + "2022-04-01, 2022-04-18", + "2022-04-18, 2022-06-20", + "2022-05-01, 2022-06-20", + "2022-06-01, 2022-06-20", + "2022-06-20, 2022-08-15", + "2022-07-01, 2022-08-15", + "2022-08-01, 2022-08-15", + "2022-08-15, 2022-10-17", + "2022-09-01, 2022-10-17", + "2022-10-01, 2022-10-17", + "2022-10-17, 2022-12-19", + "2022-11-01, 2022-12-19", + "2022-12-01, 2022-12-19", + "2022-12-19, 2023-02-20" + }) + public void getNextReleaseDateWhenBoundaryConditionsThenSuccess(LocalDate startDate, LocalDate expectedDate) { + SpringReleaseTrainSpec releaseTrainSpec = + SpringReleaseTrainSpec.builder() + .train(1) + .version("1.0.0") + .weekOfMonth(3) + .dayOfWeek(1) + .year(2022) + .build(); + + SpringReleaseTrain releaseTrain = new SpringReleaseTrain(releaseTrainSpec); + assertThat(releaseTrain.getNextReleaseDate(startDate)).isEqualTo(expectedDate); + } +}