Skip to content

Commit 4fc16ee

Browse files
authored
Merge pull request #155 from cloudogu/feature/153-support-release-of-multiple-major-versions
Feature/153 support release of multiple major versions
2 parents 27a31d5 + 0784042 commit 4fc16ee

File tree

6 files changed

+386
-6
lines changed

6 files changed

+386
-6
lines changed

CHANGELOG.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
77

88

99
## [Unreleased]
10+
### Changed
11+
- Set development branch on finishing gitflow release
12+
1013
## [4.3.0](https://github.com/cloudogu/ces-build-lib/releases/tag/4.3.0) - 2025-08-21
1114
### Changed
1215
- Updates the BATS shell test image to 1.12 which supports the `--report-formatter` switch

src/com/cloudogu/ces/cesbuildlib/Git.groovy

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -500,4 +500,9 @@ class Git implements Serializable {
500500
script.echo commandOutput
501501
return commandOutput
502502
}
503+
504+
boolean branchExists(String branch) {
505+
def branchFound = this.executeGitWithCredentials("show-ref refs/remotes/origin/${branch}")
506+
return branchFound != null && branchFound.length() > 0
507+
}
503508
}

src/com/cloudogu/ces/cesbuildlib/GitFlow.groovy

Lines changed: 26 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,19 @@ class GitFlow implements Serializable {
44
private def script
55
private Git git
66
Sh sh
7+
private Makefile makefile
78

89
GitFlow(script, Git git) {
910
this.script = script
1011
this.git = git
1112
this.sh = new Sh(script)
1213
}
1314

15+
GitFlow(script, Git git, Makefile makefile) {
16+
this(script, git)
17+
this.makefile = makefile
18+
}
19+
1420
/**
1521
* @return if this branch is a release branch according to git flow
1622
*/
@@ -25,14 +31,24 @@ class GitFlow implements Serializable {
2531
return git.getSimpleBranchName().equals("develop")
2632
}
2733

34+
boolean isUnallowedBackportRelease(String productionBranch, String developmentBranch) {
35+
if (makefile != null) {
36+
def baseVersion = makefile.getBaseVersion()
37+
if (baseVersion != null && baseVersion != "" && (!productionBranch.contains(baseVersion) || !developmentBranch.contains(baseVersion))) {
38+
return true
39+
}
40+
}
41+
return false
42+
}
43+
2844
/**
2945
* Finishes a git flow release and pushes all merged branches to remote
3046
*
3147
* Only execute this function if you are already on a release branch
3248
*
3349
* @param releaseVersion the version that is going to be released
3450
*/
35-
void finishRelease(String releaseVersion, String productionBranch = "master") {
51+
void finishRelease(String releaseVersion, String productionBranch = "master", String developmentBranch = "develop") {
3652
String branchName = git.getBranchName()
3753

3854
// Stop the build here if there is already a tag for this version on remote.
@@ -45,8 +61,13 @@ class GitFlow implements Serializable {
4561
// Make sure all branches are fetched
4662
git.fetch()
4763

64+
// Check if a backport release is configured by setting BASE_VERSION inside Makefile
65+
if (isUnallowedBackportRelease(productionBranch, developmentBranch)) {
66+
script.error('The Variable BASE_VERSION is set in the Makefile. The release should not be merged into main / master / develop or other backport branches.')
67+
}
68+
4869
// Stop the build if there are new changes on develop that are not merged into this feature branch.
49-
if (git.originBranchesHaveDiverged(branchName, 'develop')) {
70+
if (git.originBranchesHaveDiverged(branchName, developmentBranch)) {
5071
script.error('There are changes on develop branch that are not merged into release. Please merge and restart process.')
5172
}
5273

@@ -56,7 +77,7 @@ class GitFlow implements Serializable {
5677
String releaseBranchAuthor = git.commitAuthorName
5778
String releaseBranchEmail = git.commitAuthorEmail
5879

59-
git.checkoutLatest('develop')
80+
git.checkoutLatest(developmentBranch)
6081
git.checkoutLatest(productionBranch)
6182

6283
// Merge release branch into productionBranch
@@ -65,7 +86,7 @@ class GitFlow implements Serializable {
6586
// Create tag. Use -f because the created tag will persist when build has failed.
6687
git.setTag(releaseVersion, "release version ${releaseVersion}", true)
6788
// Merge release branch into develop
68-
git.checkout('develop')
89+
git.checkout(developmentBranch)
6990
// Set author of release Branch as author of merge commit
7091
// Otherwise the author of the last commit on develop would author the commit, which is unexpected
7192
git.mergeNoFastForward(branchName, releaseBranchAuthor, releaseBranchEmail)
@@ -77,7 +98,7 @@ class GitFlow implements Serializable {
7798
git.checkout(releaseVersion)
7899

79100
// Push changes and tags
80-
git.push("origin ${productionBranch} develop ${releaseVersion}")
101+
git.push("origin ${productionBranch} ${developmentBranch} ${releaseVersion}")
81102
git.deleteOriginBranch(branchName)
82103
}
83104
}

src/com/cloudogu/ces/cesbuildlib/Makefile.groovy

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,4 +20,35 @@ class Makefile {
2020
String getVersion() {
2121
return sh.returnStdOut('grep -e "^VERSION=" Makefile | sed "s/VERSION=//g"')
2222
}
23+
24+
/**
25+
* Retrieves the value of the BASE_VERSION Variable defined in the Makefile.
26+
*/
27+
String getBaseVersion() {
28+
return sh.returnStdOut('grep -e "^BASE_VERSION=" Makefile | sed "s/BASE_VERSION=//g"')
29+
}
30+
31+
/**
32+
* Determines the develop branch for Git Flow based on the base version.
33+
*/
34+
String determineGitFlowDevelopBranch() {
35+
def develop = "develop"
36+
def baseVersion = getBaseVersion()
37+
if (baseVersion != null && baseVersion != "") {
38+
return baseVersion + "/" + develop
39+
}
40+
return develop
41+
}
42+
43+
/**
44+
* Determines the main branch for Git Flow based on the base version.
45+
*/
46+
String determineGitFlowMainBranch(defaultBranch="main") {
47+
def baseVersion = getBaseVersion()
48+
if (baseVersion != null && baseVersion != "") {
49+
// The master branch is legacy so we don't create one here, even if it was passed as parameter.
50+
return baseVersion + "/main"
51+
}
52+
return defaultBranch
53+
}
2354
}

test/com/cloudogu/ces/cesbuildlib/GitFlowTest.groovy

Lines changed: 222 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -140,6 +140,179 @@ class GitFlowTest {
140140
assertEquals("git push --delete origin myReleaseBranch", scriptMock.allActualArgs[i++])
141141
}
142142

143+
@Test
144+
void testFinishReleaseWithCustomMainAndDevelopBranch() {
145+
String releaseBranchAuthorName = 'release'
146+
String releaseBranchEmail = 'rele@s.e'
147+
String releaseBranchAuthor = createGitAuthorString(releaseBranchAuthorName, releaseBranchEmail)
148+
String developBranchAuthorName = 'develop'
149+
String developBranchEmail = 'develop@a.a'
150+
String developBranchAuthor = createGitAuthorString(developBranchAuthorName, developBranchEmail)
151+
scriptMock.expectedShRetValueForScript.put('git --no-pager show -s --format=\'%an <%ae>\' HEAD',
152+
[releaseBranchAuthor, releaseBranchAuthor, releaseBranchAuthor, releaseBranchAuthor,
153+
// these two are the ones where the release branch author is stored:
154+
releaseBranchAuthor, releaseBranchAuthor,
155+
developBranchAuthor, developBranchAuthor
156+
])
157+
scriptMock.expectedShRetValueForScript.put('git push origin main/1.0 dev/1.0 myVersion', 0)
158+
159+
scriptMock.expectedDefaultShRetValue = ""
160+
scriptMock.env.BRANCH_NAME = "myReleaseBranch"
161+
Git git = new Git(scriptMock)
162+
GitFlow gitflow = new GitFlow(scriptMock, git)
163+
gitflow.finishRelease("myVersion", "main/1.0", "dev/1.0")
164+
165+
scriptMock.allActualArgs.removeAll("echo ")
166+
scriptMock.allActualArgs.removeAll("git --no-pager show -s --format='%an <%ae>' HEAD")
167+
int i = 0
168+
assertEquals("git ls-remote origin refs/tags/myVersion", scriptMock.allActualArgs[i++])
169+
assertEquals("git config 'remote.origin.fetch' '+refs/heads/*:refs/remotes/origin/*'", scriptMock.allActualArgs[i++])
170+
assertEquals("git fetch --all", scriptMock.allActualArgs[i++])
171+
assertEquals("git log origin/myReleaseBranch..origin/dev/1.0 --oneline", scriptMock.allActualArgs[i++])
172+
assertEquals("git checkout myReleaseBranch", scriptMock.allActualArgs[i++])
173+
assertEquals("git reset --hard origin/myReleaseBranch", scriptMock.allActualArgs[i++])
174+
assertEquals("git checkout dev/1.0", scriptMock.allActualArgs[i++])
175+
assertEquals("git reset --hard origin/dev/1.0", scriptMock.allActualArgs[i++])
176+
assertEquals("git checkout main/1.0", scriptMock.allActualArgs[i++])
177+
assertEquals("git reset --hard origin/main/1.0", scriptMock.allActualArgs[i++])
178+
179+
// Author & Email 1 (calls 'git --no-pager...' twice)
180+
assertEquals("git merge --no-ff myReleaseBranch", scriptMock.allActualArgs[i++])
181+
assertAuthor(0, releaseBranchAuthorName, releaseBranchEmail)
182+
183+
// Author & Email 2 (calls 'git --no-pager...' twice)
184+
assertEquals("git tag -f -m \"release version myVersion\" myVersion", scriptMock.allActualArgs[i++])
185+
assertAuthor(1, releaseBranchAuthorName, releaseBranchEmail)
186+
187+
assertEquals("git checkout dev/1.0", scriptMock.allActualArgs[i++])
188+
// Author & Email 3 (calls 'git --no-pager...' twice)
189+
assertEquals("git merge --no-ff myReleaseBranch", scriptMock.allActualArgs[i++])
190+
assertAuthor(2, releaseBranchAuthorName, releaseBranchEmail)
191+
192+
assertEquals("git branch -d myReleaseBranch", scriptMock.allActualArgs[i++])
193+
assertEquals("git checkout myVersion", scriptMock.allActualArgs[i++])
194+
assertEquals("git push origin main/1.0 dev/1.0 myVersion", scriptMock.allActualArgs[i++])
195+
assertEquals("git push --delete origin myReleaseBranch", scriptMock.allActualArgs[i++])
196+
}
197+
198+
@Test
199+
void testFinishReleaseWithBaseVersionSet() {
200+
String releaseBranchAuthorName = 'release'
201+
String releaseBranchEmail = 'rele@s.e'
202+
String releaseBranchAuthor = createGitAuthorString(releaseBranchAuthorName, releaseBranchEmail)
203+
String developBranchAuthorName = 'develop'
204+
String developBranchEmail = 'develop@a.a'
205+
String developBranchAuthor = createGitAuthorString(developBranchAuthorName, developBranchEmail)
206+
scriptMock.expectedShRetValueForScript.put('git --no-pager show -s --format=\'%an <%ae>\' HEAD',
207+
[releaseBranchAuthor, releaseBranchAuthor, releaseBranchAuthor, releaseBranchAuthor,
208+
// these two are the ones where the release branch author is stored:
209+
releaseBranchAuthor, releaseBranchAuthor,
210+
developBranchAuthor, developBranchAuthor
211+
])
212+
scriptMock.expectedShRetValueForScript.put('git push origin 4.2.2/main 4.2.2/develop myVersion', 0)
213+
214+
scriptMock.expectedDefaultShRetValue = ""
215+
scriptMock.env.BRANCH_NAME = "myReleaseBranch"
216+
scriptMock.expectedShRetValueForScript.put('grep -e "^BASE_VERSION=" Makefile | sed "s/BASE_VERSION=//g"'.toString(), "4.2.2".toString())
217+
218+
Git git = new Git(scriptMock)
219+
Makefile makefile = new Makefile(scriptMock)
220+
GitFlow gitflow = new GitFlow(scriptMock, git, makefile)
221+
gitflow.finishRelease("myVersion", "4.2.2/main", "4.2.2/develop")
222+
223+
scriptMock.allActualArgs.removeAll("echo ")
224+
scriptMock.allActualArgs.removeAll("git --no-pager show -s --format='%an <%ae>' HEAD")
225+
int i = 0
226+
assertEquals("git ls-remote origin refs/tags/myVersion", scriptMock.allActualArgs[i++])
227+
assertEquals("git config 'remote.origin.fetch' '+refs/heads/*:refs/remotes/origin/*'", scriptMock.allActualArgs[i++])
228+
assertEquals("git fetch --all", scriptMock.allActualArgs[i++])
229+
assertEquals("grep -e \"^BASE_VERSION=\" Makefile | sed \"s/BASE_VERSION=//g\"", scriptMock.allActualArgs[i++])
230+
assertEquals("git log origin/myReleaseBranch..origin/4.2.2/develop --oneline", scriptMock.allActualArgs[i++])
231+
assertEquals("git checkout myReleaseBranch", scriptMock.allActualArgs[i++])
232+
assertEquals("git reset --hard origin/myReleaseBranch", scriptMock.allActualArgs[i++])
233+
assertEquals("git checkout 4.2.2/develop", scriptMock.allActualArgs[i++])
234+
assertEquals("git reset --hard origin/4.2.2/develop", scriptMock.allActualArgs[i++])
235+
assertEquals("git checkout 4.2.2/main", scriptMock.allActualArgs[i++])
236+
assertEquals("git reset --hard origin/4.2.2/main", scriptMock.allActualArgs[i++])
237+
238+
// Author & Email 1 (calls 'git --no-pager...' twice)
239+
assertEquals("git merge --no-ff myReleaseBranch", scriptMock.allActualArgs[i++])
240+
assertAuthor(0, releaseBranchAuthorName, releaseBranchEmail)
241+
242+
// Author & Email 2 (calls 'git --no-pager...' twice)
243+
assertEquals("git tag -f -m \"release version myVersion\" myVersion", scriptMock.allActualArgs[i++])
244+
assertAuthor(1, releaseBranchAuthorName, releaseBranchEmail)
245+
246+
assertEquals("git checkout 4.2.2/develop", scriptMock.allActualArgs[i++])
247+
// Author & Email 3 (calls 'git --no-pager...' twice)
248+
assertEquals("git merge --no-ff myReleaseBranch", scriptMock.allActualArgs[i++])
249+
assertAuthor(2, releaseBranchAuthorName, releaseBranchEmail)
250+
251+
assertEquals("git branch -d myReleaseBranch", scriptMock.allActualArgs[i++])
252+
assertEquals("git checkout myVersion", scriptMock.allActualArgs[i++])
253+
assertEquals("git push origin 4.2.2/main 4.2.2/develop myVersion", scriptMock.allActualArgs[i++])
254+
assertEquals("git push --delete origin myReleaseBranch", scriptMock.allActualArgs[i++])
255+
}
256+
257+
@Test
258+
void testFinishReleaseWithBaseVersionUnSet() {
259+
String releaseBranchAuthorName = 'release'
260+
String releaseBranchEmail = 'rele@s.e'
261+
String releaseBranchAuthor = createGitAuthorString(releaseBranchAuthorName, releaseBranchEmail)
262+
String developBranchAuthorName = 'develop'
263+
String developBranchEmail = 'develop@a.a'
264+
String developBranchAuthor = createGitAuthorString(developBranchAuthorName, developBranchEmail)
265+
scriptMock.expectedShRetValueForScript.put('git --no-pager show -s --format=\'%an <%ae>\' HEAD',
266+
[releaseBranchAuthor, releaseBranchAuthor, releaseBranchAuthor, releaseBranchAuthor,
267+
// these two are the ones where the release branch author is stored:
268+
releaseBranchAuthor, releaseBranchAuthor,
269+
developBranchAuthor, developBranchAuthor
270+
])
271+
scriptMock.expectedShRetValueForScript.put('git push origin master develop myVersion', 0)
272+
273+
scriptMock.expectedDefaultShRetValue = ""
274+
scriptMock.env.BRANCH_NAME = "myReleaseBranch"
275+
scriptMock.expectedShRetValueForScript.put('grep -e "^BASE_VERSION=" Makefile | sed "s/BASE_VERSION=//g"'.toString(), "".toString())
276+
277+
Git git = new Git(scriptMock)
278+
Makefile makefile = new Makefile(scriptMock)
279+
GitFlow gitflow = new GitFlow(scriptMock, git, makefile)
280+
gitflow.finishRelease("myVersion")
281+
282+
scriptMock.allActualArgs.removeAll("echo ")
283+
scriptMock.allActualArgs.removeAll("git --no-pager show -s --format='%an <%ae>' HEAD")
284+
int i = 0
285+
assertEquals("git ls-remote origin refs/tags/myVersion", scriptMock.allActualArgs[i++])
286+
assertEquals("git config 'remote.origin.fetch' '+refs/heads/*:refs/remotes/origin/*'", scriptMock.allActualArgs[i++])
287+
assertEquals("git fetch --all", scriptMock.allActualArgs[i++])
288+
assertEquals("grep -e \"^BASE_VERSION=\" Makefile | sed \"s/BASE_VERSION=//g\"", scriptMock.allActualArgs[i++])
289+
assertEquals("git log origin/myReleaseBranch..origin/develop --oneline", scriptMock.allActualArgs[i++])
290+
assertEquals("git checkout myReleaseBranch", scriptMock.allActualArgs[i++])
291+
assertEquals("git reset --hard origin/myReleaseBranch", scriptMock.allActualArgs[i++])
292+
assertEquals("git checkout develop", scriptMock.allActualArgs[i++])
293+
assertEquals("git reset --hard origin/develop", scriptMock.allActualArgs[i++])
294+
assertEquals("git checkout master", scriptMock.allActualArgs[i++])
295+
assertEquals("git reset --hard origin/master", scriptMock.allActualArgs[i++])
296+
297+
// Author & Email 1 (calls 'git --no-pager...' twice)
298+
assertEquals("git merge --no-ff myReleaseBranch", scriptMock.allActualArgs[i++])
299+
assertAuthor(0, releaseBranchAuthorName, releaseBranchEmail)
300+
301+
// Author & Email 2 (calls 'git --no-pager...' twice)
302+
assertEquals("git tag -f -m \"release version myVersion\" myVersion", scriptMock.allActualArgs[i++])
303+
assertAuthor(1, releaseBranchAuthorName, releaseBranchEmail)
304+
305+
assertEquals("git checkout develop", scriptMock.allActualArgs[i++])
306+
// Author & Email 3 (calls 'git --no-pager...' twice)
307+
assertEquals("git merge --no-ff myReleaseBranch", scriptMock.allActualArgs[i++])
308+
assertAuthor(2, releaseBranchAuthorName, releaseBranchEmail)
309+
310+
assertEquals("git branch -d myReleaseBranch", scriptMock.allActualArgs[i++])
311+
assertEquals("git checkout myVersion", scriptMock.allActualArgs[i++])
312+
assertEquals("git push origin master develop myVersion", scriptMock.allActualArgs[i++])
313+
assertEquals("git push --delete origin myReleaseBranch", scriptMock.allActualArgs[i++])
314+
}
315+
143316
@Test
144317
void testThrowsErrorWhenTagAlreadyExists() {
145318
scriptMock.expectedShRetValueForScript.put('git ls-remote origin refs/tags/myVersion', 'thisIsATag')
@@ -163,6 +336,55 @@ class GitFlowTest {
163336
assertEquals("There are changes on develop branch that are not merged into release. Please merge and restart process.", err.getMessage())
164337
}
165338

339+
@Test
340+
void testIsUnallowedBackportReleaseIsUnallowedStandardBranches() {
341+
scriptMock.expectedShRetValueForScript.put('grep -e "^BASE_VERSION=" Makefile | sed "s/BASE_VERSION=//g"'.toString(), "4.2".toString())
342+
343+
Git git = new Git(scriptMock)
344+
Makefile makefile = new Makefile(scriptMock)
345+
GitFlow gitflow = new GitFlow(scriptMock, git, makefile)
346+
def result = gitflow.isUnallowedBackportRelease("main", "develop")
347+
348+
assertEquals(true, result)
349+
}
350+
351+
@Test
352+
void testIsUnallowedBackportReleaseIsUnallowedStandardBranches2() {
353+
scriptMock.expectedShRetValueForScript.put('grep -e "^BASE_VERSION=" Makefile | sed "s/BASE_VERSION=//g"'.toString(), "4.2".toString())
354+
355+
Git git = new Git(scriptMock)
356+
Makefile makefile = new Makefile(scriptMock)
357+
GitFlow gitflow = new GitFlow(scriptMock, git, makefile)
358+
def result = gitflow.isUnallowedBackportRelease("master", "develop")
359+
360+
assertEquals(true, result)
361+
}
362+
363+
@Test
364+
void testIsUnallowedBackportReleaseIsUnallowedWrongVersionBranches() {
365+
scriptMock.expectedShRetValueForScript.put('grep -e "^BASE_VERSION=" Makefile | sed "s/BASE_VERSION=//g"'.toString(), "4.2".toString())
366+
367+
Git git = new Git(scriptMock)
368+
Makefile makefile = new Makefile(scriptMock)
369+
GitFlow gitflow = new GitFlow(scriptMock, git, makefile)
370+
def result = gitflow.isUnallowedBackportRelease("4.3/main", "4.3/develop")
371+
372+
assertEquals(true, result)
373+
}
374+
375+
@Test
376+
void testIsUnallowedBackportReleaseIsAllowed() {
377+
scriptMock.expectedShRetValueForScript.put('grep -e "^BASE_VERSION=" Makefile | sed "s/BASE_VERSION=//g"'.toString(), "4.2".toString())
378+
379+
Git git = new Git(scriptMock)
380+
Makefile makefile = new Makefile(scriptMock)
381+
GitFlow gitflow = new GitFlow(scriptMock, git, makefile)
382+
def result = gitflow.isUnallowedBackportRelease("4.2/main", "4.2/develop")
383+
384+
assertEquals(false, result)
385+
}
386+
387+
166388
void assertAuthor(int withEnvInvocationIndex, String author, String email) {
167389
def withEnvMap = scriptMock.actualWithEnvAsMap(withEnvInvocationIndex)
168390
assert withEnvMap['GIT_AUTHOR_NAME'] == author

0 commit comments

Comments
 (0)