Skip to content

Commit 84d9629

Browse files
committed
Add gitHubCheckMilestoneHasNoOpenIssues
Closes gh-9693
1 parent 23eee9a commit 84d9629

File tree

10 files changed

+1118
-3
lines changed

10 files changed

+1118
-3
lines changed

RELEASE.adoc

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -49,8 +49,8 @@ $ git checkout -
4949

5050
The following command will update the dependencies again but this time creating a ticket for each update and placing `Closes gh-<number>` in the commit. Replacing the following values:
5151

52-
<github-personal-access-token> - Replace with a https://github.com/settings/tokens[GitHub personal access token] that has a scope of `public_repo`
53-
<next-version> - Replace with the title of the milestone you are releasing now (i.e. 5.5.0-RC1)
52+
* <github-personal-access-token> - Replace with a https://github.com/settings/tokens[GitHub personal access token] that has a scope of `public_repo`
53+
* <next-version> - Replace with the title of the milestone you are releasing now (i.e. 5.5.0-RC1)
5454
5555
[source,bash]
5656
----
@@ -61,7 +61,18 @@ Apply any fixes from your previous branch that were necessary.
6161

6262
= Check All Issues are Closed
6363

64-
Check that all issues are closed for the milestone https://github.com/spring-projects/spring-security/milestones
64+
The following command will check if there are any open issues for the ticket.
65+
Before running the command, replace the following values:
66+
67+
* <github-personal-access-token> - Replace with a https://github.com/settings/tokens[GitHub personal access token] that has a scope of `public_repo`. This is optional since you are unlikely to reach the rate limit for such a simple check.
68+
* <next-version> - Replace with the title of the milestone you are releasing now (i.e. 5.5.0-RC1)
69+
70+
[source,bash]
71+
----
72+
$ ./gradlew gitHubCheckMilestoneHasNoOpenIssues -PgitHubAccessToken=<github-personal-access-token> -PnextVersion=<next-version>
73+
----
74+
75+
Alternatively, you can manually check using https://github.com/spring-projects/spring-security/milestones
6576

6677
= Update Release Version
6778

build.gradle

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ apply plugin: 'io.spring.convention.root'
1717
apply plugin: 'org.jetbrains.kotlin.jvm'
1818
apply plugin: 'org.springframework.security.update-dependencies'
1919
apply plugin: 'org.springframework.security.sagan'
20+
apply plugin: 'org.springframework.github.milestone'
2021

2122
group = 'org.springframework.security'
2223
description = 'Spring Security'
@@ -34,6 +35,13 @@ tasks.named("saganCreateRelease") {
3435
apiDocUrl = "https://docs.spring.io/spring-security/site/docs/{version}/api/"
3536
}
3637

38+
tasks.named("gitHubCheckMilestoneHasNoOpenIssues") {
39+
repository {
40+
owner = "spring-projects"
41+
name = "spring-security"
42+
}
43+
}
44+
3745
updateDependenciesSettings {
3846
gitHub {
3947
organization = "spring-projects"

buildSrc/build.gradle

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,10 @@ gradlePlugin {
4848
id = "org.springframework.security.sagan"
4949
implementationClass = "org.springframework.gradle.sagan.SaganPlugin"
5050
}
51+
githubMilestone {
52+
id = "org.springframework.github.milestone"
53+
implementationClass = "org.springframework.gradle.github.milestones.GitHubMilestonePlugin"
54+
}
5155
}
5256
}
5357

Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
/*
2+
* Copyright 2019-2020 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.gradle.github.milestones;
18+
19+
import com.google.common.reflect.TypeToken;
20+
import com.google.gson.Gson;
21+
import okhttp3.Interceptor;
22+
import okhttp3.OkHttpClient;
23+
import okhttp3.Request;
24+
import okhttp3.Response;
25+
26+
import java.io.IOException;
27+
import java.util.List;
28+
29+
public class GitHubMilestoneApi {
30+
private String baseUrl = "https://api.github.com";
31+
32+
private OkHttpClient client;
33+
34+
private Gson gson = new Gson();
35+
36+
public GitHubMilestoneApi() {
37+
this.client = new OkHttpClient.Builder().build();
38+
}
39+
40+
public GitHubMilestoneApi(String gitHubToken) {
41+
this.client = new OkHttpClient.Builder()
42+
.addInterceptor(new AuthorizationInterceptor(gitHubToken))
43+
.build();
44+
}
45+
46+
public void setBaseUrl(String baseUrl) {
47+
this.baseUrl = baseUrl;
48+
}
49+
50+
public long findMilestoneNumberByTitle(RepositoryRef repositoryRef, String milestoneTitle) {
51+
String url = this.baseUrl + "/repos/" + repositoryRef.getOwner() + "/" + repositoryRef.getName() + "/milestones?per_page=100";
52+
Request request = new Request.Builder().get().url(url)
53+
.build();
54+
try {
55+
Response response = this.client.newCall(request).execute();
56+
if (!response.isSuccessful()) {
57+
throw new RuntimeException("Could not find milestone with title " + milestoneTitle + " for repository " + repositoryRef + ". Response " + response);
58+
}
59+
List<Milestone> milestones = this.gson.fromJson(response.body().charStream(), new TypeToken<List<Milestone>>(){}.getType());
60+
for (Milestone milestone : milestones) {
61+
if (milestoneTitle.equals(milestone.getTitle())) {
62+
return milestone.getNumber();
63+
}
64+
}
65+
if (milestones.size() <= 100) {
66+
throw new RuntimeException("Could not find open milestone with title " + milestoneTitle + " for repository " + repositoryRef + " Got " + milestones);
67+
}
68+
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);
69+
} catch (IOException e) {
70+
throw new RuntimeException("Could not find open milestone with title " + milestoneTitle + " for repository " + repositoryRef, e);
71+
}
72+
}
73+
74+
public boolean isOpenIssuesForMilestoneNumber(RepositoryRef repositoryRef, long milestoneNumber) {
75+
String url = this.baseUrl + "/repos/" + repositoryRef.getOwner() + "/" + repositoryRef.getName() + "/issues?per_page=1&milestone=" + milestoneNumber;
76+
Request request = new Request.Builder().get().url(url)
77+
.build();
78+
try {
79+
Response response = this.client.newCall(request).execute();
80+
if (!response.isSuccessful()) {
81+
throw new RuntimeException("Could not find issues for milestone number " + milestoneNumber + " for repository " + repositoryRef + ". Response " + response);
82+
}
83+
List<Object> issues = this.gson.fromJson(response.body().charStream(), new TypeToken<List<Object>>(){}.getType());
84+
return !issues.isEmpty();
85+
} catch (IOException e) {
86+
throw new RuntimeException("Could not find issues for milestone number " + milestoneNumber + " for repository " + repositoryRef, e);
87+
}
88+
}
89+
90+
// public boolean isOpenIssuesForMilestoneName(String owner, String repository, String milestoneName) {
91+
//
92+
// }
93+
94+
95+
private static class AuthorizationInterceptor implements Interceptor {
96+
97+
private final String token;
98+
99+
public AuthorizationInterceptor(String token) {
100+
this.token = token;
101+
}
102+
103+
@Override
104+
public okhttp3.Response intercept(Chain chain) throws IOException {
105+
Request request = chain.request().newBuilder()
106+
.addHeader("Authorization", "Bearer " + this.token).build();
107+
return chain.proceed(request);
108+
}
109+
}
110+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
/*
2+
* Copyright 2019-2020 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package org.springframework.gradle.github.milestones;
17+
18+
import org.gradle.api.Action;
19+
import org.gradle.api.DefaultTask;
20+
import org.gradle.api.tasks.Input;
21+
import org.gradle.api.tasks.Optional;
22+
import org.gradle.api.tasks.TaskAction;
23+
24+
public class GitHubMilestoneHasNoOpenIssuesTask extends DefaultTask {
25+
@Input
26+
private RepositoryRef repository = new RepositoryRef();
27+
28+
@Input
29+
private String milestoneTitle;
30+
31+
@Input @Optional
32+
private String gitHubAccessToken;
33+
34+
private GitHubMilestoneApi milestones = new GitHubMilestoneApi();
35+
36+
@TaskAction
37+
public void checkHasNoOpenIssues() {
38+
long milestoneNumber = this.milestones.findMilestoneNumberByTitle(this.repository, this.milestoneTitle);
39+
boolean isOpenIssues = this.milestones.isOpenIssuesForMilestoneNumber(this.repository, milestoneNumber);
40+
if (isOpenIssues) {
41+
throw new IllegalStateException("The repository " + this.repository + " has open issues for milestone with the title " + this.milestoneTitle + " and number " + milestoneNumber);
42+
}
43+
System.out.println("The repository " + this.repository + " has no open issues for milestone with the title " + this.milestoneTitle + " and number " + milestoneNumber);
44+
}
45+
46+
public RepositoryRef getRepository() {
47+
return repository;
48+
}
49+
50+
public void repository(Action<RepositoryRef> repository) {
51+
repository.execute(this.repository);
52+
}
53+
54+
public void setRepository(RepositoryRef repository) {
55+
this.repository = repository;
56+
}
57+
58+
public String getMilestoneTitle() {
59+
return milestoneTitle;
60+
}
61+
62+
public void setMilestoneTitle(String milestoneTitle) {
63+
this.milestoneTitle = milestoneTitle;
64+
}
65+
66+
public String getGitHubAccessToken() {
67+
return gitHubAccessToken;
68+
}
69+
70+
public void setGitHubAccessToken(String gitHubAccessToken) {
71+
this.gitHubAccessToken = gitHubAccessToken;
72+
this.milestones = new GitHubMilestoneApi(gitHubAccessToken);
73+
}
74+
}
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
/*
2+
* Copyright 2019-2020 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.gradle.github.milestones;
18+
19+
import org.gradle.api.Action;
20+
import org.gradle.api.Plugin;
21+
import org.gradle.api.Project;
22+
23+
public class GitHubMilestonePlugin implements Plugin<Project> {
24+
@Override
25+
public void apply(Project project) {
26+
project.getTasks().register("gitHubCheckMilestoneHasNoOpenIssues", GitHubMilestoneHasNoOpenIssuesTask.class, new Action<GitHubMilestoneHasNoOpenIssuesTask>() {
27+
@Override
28+
public void execute(GitHubMilestoneHasNoOpenIssuesTask githubCheckMilestoneHasNoOpenIssues) {
29+
githubCheckMilestoneHasNoOpenIssues.setGroup("Release");
30+
githubCheckMilestoneHasNoOpenIssues.setDescription("Checks if there are any open issues for the specified repository and milestone");
31+
githubCheckMilestoneHasNoOpenIssues.setMilestoneTitle((String) project.findProperty("nextVersion"));
32+
if (project.hasProperty("githubAccessToken")) {
33+
githubCheckMilestoneHasNoOpenIssues.setGitHubAccessToken((String) project.findProperty("gitHubAccessToken"));
34+
}
35+
}
36+
});
37+
}
38+
}
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
package org.springframework.gradle.github.milestones;
2+
3+
public class Milestone {
4+
private String title;
5+
6+
private long number;
7+
8+
public String getTitle() {
9+
return title;
10+
}
11+
12+
public void setTitle(String title) {
13+
this.title = title;
14+
}
15+
16+
public long getNumber() {
17+
return number;
18+
}
19+
20+
public void setNumber(long number) {
21+
this.number = number;
22+
}
23+
24+
@Override
25+
public String toString() {
26+
return "Milestone{" +
27+
"title='" + title + '\'' +
28+
", number=" + number +
29+
'}';
30+
}
31+
}
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
package org.springframework.gradle.github.milestones;
2+
public class RepositoryRef {
3+
private String owner;
4+
5+
private String name;
6+
7+
RepositoryRef() {
8+
}
9+
10+
public RepositoryRef(String owner, String name) {
11+
this.owner = owner;
12+
this.name = name;
13+
}
14+
15+
public String getOwner() {
16+
return owner;
17+
}
18+
19+
public void setOwner(String owner) {
20+
this.owner = owner;
21+
}
22+
23+
public String getName() {
24+
return name;
25+
}
26+
27+
public void setName(String name) {
28+
this.name = name;
29+
}
30+
31+
@Override
32+
public String toString() {
33+
return "RepositoryRef{" +
34+
"owner='" + owner + '\'' +
35+
", name='" + name + '\'' +
36+
'}';
37+
}
38+
39+
public static RepositoryRefBuilder owner(String owner) {
40+
return new RepositoryRefBuilder().owner(owner);
41+
}
42+
43+
public static final class RepositoryRefBuilder {
44+
private String owner;
45+
private String repository;
46+
47+
private RepositoryRefBuilder() {
48+
}
49+
50+
private RepositoryRefBuilder owner(String owner) {
51+
this.owner = owner;
52+
return this;
53+
}
54+
55+
public RepositoryRefBuilder repository(String repository) {
56+
this.repository = repository;
57+
return this;
58+
}
59+
60+
public RepositoryRef build() {
61+
return new RepositoryRef(owner, repository);
62+
}
63+
}
64+
}
65+

0 commit comments

Comments
 (0)