Skip to content

Commit 1a1ef42

Browse files
committed
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
1 parent 6f0029f commit 1a1ef42

File tree

9 files changed

+1315
-3
lines changed

9 files changed

+1315
-3
lines changed
Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
name: Release Next Version
2+
3+
on:
4+
push:
5+
schedule:
6+
- cron: '0 0 * * MON' # Every Monday
7+
workflow_dispatch: # Manual trigger
8+
9+
env:
10+
SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }}
11+
GRADLE_ENTERPRISE_CACHE_USER: ${{ secrets.GRADLE_ENTERPRISE_CACHE_USER }}
12+
GRADLE_ENTERPRISE_CACHE_PASSWORD: ${{ secrets.GRADLE_ENTERPRISE_CACHE_PASSWORD }}
13+
GRADLE_ENTERPRISE_SECRET_ACCESS_KEY: ${{ secrets.GRADLE_ENTERPRISE_SECRET_ACCESS_KEY }}
14+
RUN_JOBS: ${{ github.repository == 'spring-projects/spring-security' }}
15+
16+
jobs:
17+
prerequisites:
18+
name: Pre-requisites for building
19+
runs-on: ubuntu-latest
20+
outputs:
21+
runjobs: ${{ steps.continue.outputs.runjobs }}
22+
steps:
23+
- id: continue
24+
name: Determine if should continue
25+
if: env.RUN_JOBS == 'true'
26+
run: echo "::set-output name=runjobs::true"
27+
check_release_due:
28+
name: Check if the release is due today
29+
needs: [prerequisites]
30+
runs-on: ubuntu-latest
31+
if: needs.prerequisites.outputs.runjobs
32+
steps:
33+
- uses: actions/checkout@v2
34+
- name: Set up JDK 17
35+
uses: actions/setup-java@v1
36+
with:
37+
java-version: '17'
38+
- name: Setup gradle user name
39+
run: |
40+
mkdir -p ~/.gradle
41+
echo 'systemProp.user.name=spring-builds+github' >> ~/.gradle/gradle.properties
42+
- name: Cache Gradle packages
43+
uses: actions/cache@v2
44+
with:
45+
path: ~/.gradle/caches
46+
key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle') }}
47+
- name: Check release
48+
env:
49+
GRADLE_ENTERPRISE_CACHE_USERNAME: ${{ secrets.GRADLE_ENTERPRISE_CACHE_USER }}
50+
GRADLE_ENTERPRISE_CACHE_PASSWORD: ${{ secrets.GRADLE_ENTERPRISE_CACHE_PASSWORD }}
51+
GRADLE_ENTERPRISE_ACCESS_KEY: ${{ secrets.GRADLE_ENTERPRISE_SECRET_ACCESS_KEY }}
52+
run: ./gradlew gitHubCheckNextVersionDueToday
53+
release:
54+
name: Release next version
55+
needs: [check_release_due]
56+
runs-on: ubuntu-latest
57+
steps:
58+
- uses: actions/checkout@v2
59+
- name: Set up JDK
60+
uses: actions/setup-java@v1
61+
with:
62+
java-version: '17'
63+
- name: Setup gradle user name
64+
run: |
65+
mkdir -p ~/.gradle
66+
echo 'systemProp.user.name=spring-builds+github' >> ~/.gradle/gradle.properties
67+
- name: Deploy artifacts
68+
run: |
69+
export GRADLE_ENTERPRISE_CACHE_USERNAME="$GRADLE_ENTERPRISE_CACHE_USER"
70+
export GRADLE_ENTERPRISE_CACHE_PASSWORD="$GRADLE_ENTERPRISE_CACHE_PASSWORD"
71+
export GRADLE_ENTERPRISE_ACCESS_KEY="$GRADLE_ENTERPRISE_SECRET_ACCESS_KEY"
72+
echo "Release task: use input from gitHubNextReleaseMilestone task"
73+
./gradlew gitHubNextReleaseMilestone
74+
env:
75+
ORG_GRADLE_PROJECT_signingKey: ${{ secrets.GPG_PRIVATE_KEY }}
76+
ORG_GRADLE_PROJECT_signingPassword: ${{ secrets.GPG_PASSPHRASE }}
77+
OSSRH_TOKEN_USERNAME: ${{ secrets.OSSRH_S01_TOKEN_USERNAME }}
78+
OSSRH_TOKEN_PASSWORD: ${{ secrets.OSSRH_S01_TOKEN_PASSWORD }}
79+
notify_result:
80+
name: Check for failures
81+
needs: [release]
82+
if: failure()
83+
runs-on: ubuntu-latest
84+
steps:
85+
- name: Send Slack message
86+
uses: Gamesight/[email protected]
87+
with:
88+
repo_token: ${{ secrets.GITHUB_TOKEN }}
89+
slack_webhook_url: ${{ secrets.SLACK_WEBHOOK_URL }}
90+
channel: '#spring-security-ci'
91+
name: 'CI Notifier'

build.gradle

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,20 @@ tasks.named("gitHubCheckMilestoneHasNoOpenIssues") {
4747
}
4848
}
4949

50+
tasks.named("gitHubNextReleaseMilestone") {
51+
repository {
52+
owner = "spring-projects"
53+
name = "spring-security"
54+
}
55+
}
56+
57+
tasks.named("gitHubCheckNextVersionDueToday") {
58+
repository {
59+
owner = "spring-projects"
60+
name = "spring-security"
61+
}
62+
}
63+
5064
tasks.named("createGitHubRelease") {
5165
repository {
5266
owner = "spring-projects"

buildSrc/src/main/java/org/springframework/gradle/github/milestones/GitHubMilestoneApi.java

Lines changed: 127 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2019-2020 the original author or authors.
2+
* Copyright 2019-2022 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -17,7 +17,11 @@
1717
package org.springframework.gradle.github.milestones;
1818

1919
import java.io.IOException;
20+
import java.time.Instant;
2021
import java.util.List;
22+
import java.util.Optional;
23+
import java.util.regex.Matcher;
24+
import java.util.regex.Pattern;
2125

2226
import com.google.common.reflect.TypeToken;
2327
import com.google.gson.Gson;
@@ -89,6 +93,128 @@ public boolean isOpenIssuesForMilestoneNumber(RepositoryRef repositoryRef, long
8993
}
9094
}
9195

96+
/**
97+
* Check if the given milestone is due today or past due.
98+
*
99+
* @param repositoryRef The repository owner/name
100+
* @param milestoneTitle The title of the milestone whose due date should be checked
101+
* @return true if the given milestone is due today or past due, false otherwise
102+
*/
103+
public boolean isMilestoneDueToday(RepositoryRef repositoryRef, String milestoneTitle) {
104+
String url = this.baseUrl + "/repos/" + repositoryRef.getOwner() + "/" + repositoryRef.getName()
105+
+ "/milestones?per_page=100";
106+
Request request = new Request.Builder().get().url(url).build();
107+
try {
108+
Response response = this.client.newCall(request).execute();
109+
if (!response.isSuccessful()) {
110+
throw new RuntimeException("Could not find milestone with title " + milestoneTitle + " for repository "
111+
+ repositoryRef + ". Response " + response);
112+
}
113+
List<Milestone> milestones = this.gson.fromJson(response.body().charStream(),
114+
new TypeToken<List<Milestone>>() {
115+
}.getType());
116+
for (Milestone milestone : milestones) {
117+
if (milestoneTitle.equals(milestone.getTitle())) {
118+
Instant now = Instant.now();
119+
return milestone.getDueOn() != null && now.isAfter(milestone.getDueOn().toInstant());
120+
}
121+
}
122+
if (milestones.size() <= 100) {
123+
throw new RuntimeException("Could not find open milestone with title " + milestoneTitle
124+
+ " for repository " + repositoryRef + " Got " + milestones);
125+
}
126+
throw new RuntimeException(
127+
"It is possible there are too many open milestones open (only 100 are supported). Could not find open milestone with title "
128+
+ milestoneTitle + " for repository " + repositoryRef + " Got " + milestones);
129+
}
130+
catch (IOException e) {
131+
throw new RuntimeException(
132+
"Could not find open milestone with title " + milestoneTitle + " for repository " + repositoryRef,
133+
e);
134+
}
135+
}
136+
137+
/**
138+
* Calculate the next release version based on the current version.
139+
*
140+
* The current version must conform to the pattern MAJOR.MINOR.PATCH-SNAPSHOT. If the
141+
* current version is a snapshot of a patch release, then the patch release will be
142+
* returned. For example, if the current version is 5.6.1-SNAPSHOT, then 5.6.1 will be
143+
* returned. If the current version is a snapshot of a version that is not GA (i.e the
144+
* PATCH segment is 0), then GitHub will be queried to find the next milestone or
145+
* release candidate. If no pre-release versions are found, then the next version will
146+
* be assumed to be the GA.
147+
* @param repositoryRef The repository owner/name
148+
* @param currentVersion The current project version
149+
* @return the next matching milestone/release candidate or null if none exist
150+
*/
151+
public String getNextReleaseMilestone(RepositoryRef repositoryRef, String currentVersion) {
152+
Pattern snapshotPattern = Pattern.compile("^([0-9]+)\\.([0-9]+)\\.([0-9]+)-SNAPSHOT$");
153+
Matcher snapshotVersion = snapshotPattern.matcher(currentVersion);
154+
155+
if (snapshotVersion.find()) {
156+
String patchSegment = snapshotVersion.group(3);
157+
String currentVersionNoIdentifier = currentVersion.replace("-SNAPSHOT", "");
158+
if (patchSegment.equals("0")) {
159+
String nextPreRelease = getNextPreRelease(repositoryRef, currentVersionNoIdentifier);
160+
return nextPreRelease != null ? nextPreRelease : currentVersionNoIdentifier;
161+
}
162+
else {
163+
return currentVersionNoIdentifier;
164+
}
165+
}
166+
else {
167+
throw new IllegalStateException(
168+
"Cannot calculate next release version because the current project version does not conform to the expected format");
169+
}
170+
}
171+
172+
/**
173+
* Calculate the next pre-release version (milestone or release candidate) based on
174+
* the current version.
175+
*
176+
* The current version must conform to the pattern MAJOR.MINOR.PATCH. If no matching
177+
* milestone or release candidate is found in GitHub then it will return null.
178+
* @param repositoryRef The repository owner/name
179+
* @param currentVersionNoIdentifier The current project version without any
180+
* identifier
181+
* @return the next matching milestone/release candidate or null if none exist
182+
*/
183+
private String getNextPreRelease(RepositoryRef repositoryRef, String currentVersionNoIdentifier) {
184+
String url = this.baseUrl + "/repos/" + repositoryRef.getOwner() + "/" + repositoryRef.getName()
185+
+ "/milestones?per_page=100";
186+
Request request = new Request.Builder().get().url(url).build();
187+
try {
188+
Response response = this.client.newCall(request).execute();
189+
if (!response.isSuccessful()) {
190+
throw new RuntimeException(
191+
"Could not get milestones for repository " + repositoryRef + ". Response " + response);
192+
}
193+
List<Milestone> milestones = this.gson.fromJson(response.body().charStream(),
194+
new TypeToken<List<Milestone>>() {
195+
}.getType());
196+
Optional<String> nextPreRelease = milestones.stream().map(Milestone::getTitle)
197+
.filter(m -> m.startsWith(currentVersionNoIdentifier + "-"))
198+
.min((m1, m2) -> {
199+
Pattern preReleasePattern = Pattern.compile("^.*-([A-Z]+)([0-9]+)$");
200+
Matcher matcher1 = preReleasePattern.matcher(m1);
201+
Matcher matcher2 = preReleasePattern.matcher(m2);
202+
matcher1.find();
203+
matcher2.find();
204+
if (!matcher1.group(1).equals(matcher2.group(1))) {
205+
return m1.compareTo(m2);
206+
}
207+
else {
208+
return Integer.valueOf(matcher1.group(2)).compareTo(Integer.valueOf(matcher2.group(2)));
209+
}
210+
});
211+
return nextPreRelease.orElse(null);
212+
}
213+
catch (IOException e) {
214+
throw new RuntimeException("Could not find open milestones with for repository " + repositoryRef, e);
215+
}
216+
}
217+
92218
// public boolean isOpenIssuesForMilestoneName(String owner, String repository, String milestoneName) {
93219
//
94220
// }
Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
/*
2+
* Copyright 2019-2022 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.DefaultTask;
21+
import org.gradle.api.file.RegularFileProperty;
22+
import org.gradle.api.tasks.Input;
23+
import org.gradle.api.tasks.Optional;
24+
import org.gradle.api.tasks.OutputFile;
25+
import org.gradle.api.tasks.TaskAction;
26+
import org.yaml.snakeyaml.DumperOptions;
27+
import org.yaml.snakeyaml.Yaml;
28+
import org.yaml.snakeyaml.nodes.Tag;
29+
import org.yaml.snakeyaml.representer.Representer;
30+
31+
import java.io.File;
32+
import java.io.FileWriter;
33+
import java.io.IOException;
34+
35+
import org.springframework.gradle.github.RepositoryRef;
36+
37+
public abstract class GitHubMilestoneNextReleaseTask extends DefaultTask {
38+
39+
@Input
40+
private RepositoryRef repository = new RepositoryRef();
41+
42+
@Input
43+
@Optional
44+
private String gitHubAccessToken;
45+
46+
private GitHubMilestoneApi milestones = new GitHubMilestoneApi();
47+
48+
@TaskAction
49+
public void calculateNextReleaseMilestone() throws IOException {
50+
String currentVersion = getProject().getVersion().toString();
51+
String nextPreRelease = this.milestones.getNextReleaseMilestone(this.repository, currentVersion);
52+
System.out.println("The next release milestone is: " + nextPreRelease);
53+
NextVersionYml nextVersionYml = new NextVersionYml();
54+
nextVersionYml.setVersion(nextPreRelease);
55+
File outputFile = getNextReleaseFile().get().getAsFile();
56+
FileWriter outputWriter = new FileWriter(outputFile);
57+
Yaml yaml = getYaml();
58+
yaml.dump(nextVersionYml, outputWriter);
59+
}
60+
61+
@OutputFile
62+
public abstract RegularFileProperty getNextReleaseFile();
63+
64+
public RepositoryRef getRepository() {
65+
return repository;
66+
}
67+
68+
public void repository(Action<RepositoryRef> repository) {
69+
repository.execute(this.repository);
70+
}
71+
72+
public void setRepository(RepositoryRef repository) {
73+
this.repository = repository;
74+
}
75+
76+
public String getGitHubAccessToken() {
77+
return gitHubAccessToken;
78+
}
79+
80+
public void setGitHubAccessToken(String gitHubAccessToken) {
81+
this.gitHubAccessToken = gitHubAccessToken;
82+
this.milestones = new GitHubMilestoneApi(gitHubAccessToken);
83+
}
84+
85+
private Yaml getYaml() {
86+
Representer representer = new Representer();
87+
representer.addClassTag(NextVersionYml.class, Tag.MAP);
88+
DumperOptions ymlOptions = new DumperOptions();
89+
ymlOptions.setDefaultFlowStyle(DumperOptions.FlowStyle.BLOCK);
90+
return new Yaml(representer, ymlOptions);
91+
}
92+
93+
}

0 commit comments

Comments
 (0)