Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0


## [Unreleased]
### Changed
- Set development branch on finishing gitflow release

## [4.3.0](https://github.com/cloudogu/ces-build-lib/releases/tag/4.3.0) - 2025-08-21
### Changed
- Updates the BATS shell test image to 1.12 which supports the `--report-formatter` switch
Expand Down
5 changes: 5 additions & 0 deletions src/com/cloudogu/ces/cesbuildlib/Git.groovy
Original file line number Diff line number Diff line change
Expand Up @@ -500,4 +500,9 @@ class Git implements Serializable {
script.echo commandOutput
return commandOutput
}

boolean branchExists(String branch) {
def branchFound = this.executeGitWithCredentials("show-ref refs/remotes/origin/${branch}")
return branchFound != null && branchFound.length() > 0
}
}
31 changes: 26 additions & 5 deletions src/com/cloudogu/ces/cesbuildlib/GitFlow.groovy
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,19 @@ class GitFlow implements Serializable {
private def script
private Git git
Sh sh
private Makefile makefile

GitFlow(script, Git git) {
this.script = script
this.git = git
this.sh = new Sh(script)
}

GitFlow(script, Git git, Makefile makefile) {
this(script, git)
this.makefile = makefile
}

/**
* @return if this branch is a release branch according to git flow
*/
Expand All @@ -25,14 +31,24 @@ class GitFlow implements Serializable {
return git.getSimpleBranchName().equals("develop")
}

boolean isUnallowedBackportRelease(String productionBranch, String developmentBranch) {
if (makefile != null) {
def baseVersion = makefile.getBaseVersion()
if (baseVersion != null && baseVersion != "" && (!productionBranch.contains(baseVersion) || !developmentBranch.contains(baseVersion))) {
return true
}
}
return false
}

/**
* Finishes a git flow release and pushes all merged branches to remote
*
* Only execute this function if you are already on a release branch
*
* @param releaseVersion the version that is going to be released
*/
void finishRelease(String releaseVersion, String productionBranch = "master") {
void finishRelease(String releaseVersion, String productionBranch = "master", String developmentBranch = "develop") {
String branchName = git.getBranchName()

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

// Check if a backport release is configured by setting BASE_VERSION inside Makefile
if (isUnallowedBackportRelease(productionBranch, developmentBranch)) {
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.')
}

// Stop the build if there are new changes on develop that are not merged into this feature branch.
if (git.originBranchesHaveDiverged(branchName, 'develop')) {
if (git.originBranchesHaveDiverged(branchName, developmentBranch)) {
script.error('There are changes on develop branch that are not merged into release. Please merge and restart process.')
}

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

git.checkoutLatest('develop')
git.checkoutLatest(developmentBranch)
git.checkoutLatest(productionBranch)

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

// Push changes and tags
git.push("origin ${productionBranch} develop ${releaseVersion}")
git.push("origin ${productionBranch} ${developmentBranch} ${releaseVersion}")
git.deleteOriginBranch(branchName)
}
}
31 changes: 31 additions & 0 deletions src/com/cloudogu/ces/cesbuildlib/Makefile.groovy
Original file line number Diff line number Diff line change
Expand Up @@ -20,4 +20,35 @@ class Makefile {
String getVersion() {
return sh.returnStdOut('grep -e "^VERSION=" Makefile | sed "s/VERSION=//g"')
}

/**
* Retrieves the value of the BASE_VERSION Variable defined in the Makefile.
*/
String getBaseVersion() {
return sh.returnStdOut('grep -e "^BASE_VERSION=" Makefile | sed "s/BASE_VERSION=//g"')
}

/**
* Determines the develop branch for Git Flow based on the base version.
*/
String determineGitFlowDevelopBranch() {
def develop = "develop"
def baseVersion = getBaseVersion()
if (baseVersion != null && baseVersion != "") {
return baseVersion + "/" + develop
}
return develop
}

/**
* Determines the main branch for Git Flow based on the base version.
*/
String determineGitFlowMainBranch(defaultBranch="main") {
def baseVersion = getBaseVersion()
if (baseVersion != null && baseVersion != "") {
// The master branch is legacy so we don't create one here, even if it was passed as parameter.
return baseVersion + "/main"
}
return defaultBranch
}
}
222 changes: 222 additions & 0 deletions test/com/cloudogu/ces/cesbuildlib/GitFlowTest.groovy
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,179 @@ class GitFlowTest {
assertEquals("git push --delete origin myReleaseBranch", scriptMock.allActualArgs[i++])
}

@Test
void testFinishReleaseWithCustomMainAndDevelopBranch() {
String releaseBranchAuthorName = 'release'
String releaseBranchEmail = '[email protected]'
String releaseBranchAuthor = createGitAuthorString(releaseBranchAuthorName, releaseBranchEmail)
String developBranchAuthorName = 'develop'
String developBranchEmail = '[email protected]'
String developBranchAuthor = createGitAuthorString(developBranchAuthorName, developBranchEmail)
scriptMock.expectedShRetValueForScript.put('git --no-pager show -s --format=\'%an <%ae>\' HEAD',
[releaseBranchAuthor, releaseBranchAuthor, releaseBranchAuthor, releaseBranchAuthor,
// these two are the ones where the release branch author is stored:
releaseBranchAuthor, releaseBranchAuthor,
developBranchAuthor, developBranchAuthor
])
Comment on lines +151 to +156
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What is happening here? :D why is that call returning 6 times release Author and two times developAuthor?

scriptMock.expectedShRetValueForScript.put('git push origin main/1.0 dev/1.0 myVersion', 0)

scriptMock.expectedDefaultShRetValue = ""
scriptMock.env.BRANCH_NAME = "myReleaseBranch"
Git git = new Git(scriptMock)
GitFlow gitflow = new GitFlow(scriptMock, git)
gitflow.finishRelease("myVersion", "main/1.0", "dev/1.0")

scriptMock.allActualArgs.removeAll("echo ")
scriptMock.allActualArgs.removeAll("git --no-pager show -s --format='%an <%ae>' HEAD")
int i = 0
assertEquals("git ls-remote origin refs/tags/myVersion", scriptMock.allActualArgs[i++])
assertEquals("git config 'remote.origin.fetch' '+refs/heads/*:refs/remotes/origin/*'", scriptMock.allActualArgs[i++])
assertEquals("git fetch --all", scriptMock.allActualArgs[i++])
assertEquals("git log origin/myReleaseBranch..origin/dev/1.0 --oneline", scriptMock.allActualArgs[i++])
assertEquals("git checkout myReleaseBranch", scriptMock.allActualArgs[i++])
assertEquals("git reset --hard origin/myReleaseBranch", scriptMock.allActualArgs[i++])
assertEquals("git checkout dev/1.0", scriptMock.allActualArgs[i++])
assertEquals("git reset --hard origin/dev/1.0", scriptMock.allActualArgs[i++])
assertEquals("git checkout main/1.0", scriptMock.allActualArgs[i++])
assertEquals("git reset --hard origin/main/1.0", scriptMock.allActualArgs[i++])

// Author & Email 1 (calls 'git --no-pager...' twice)
assertEquals("git merge --no-ff myReleaseBranch", scriptMock.allActualArgs[i++])
assertAuthor(0, releaseBranchAuthorName, releaseBranchEmail)

// Author & Email 2 (calls 'git --no-pager...' twice)
assertEquals("git tag -f -m \"release version myVersion\" myVersion", scriptMock.allActualArgs[i++])
assertAuthor(1, releaseBranchAuthorName, releaseBranchEmail)

assertEquals("git checkout dev/1.0", scriptMock.allActualArgs[i++])
// Author & Email 3 (calls 'git --no-pager...' twice)
assertEquals("git merge --no-ff myReleaseBranch", scriptMock.allActualArgs[i++])
assertAuthor(2, releaseBranchAuthorName, releaseBranchEmail)

assertEquals("git branch -d myReleaseBranch", scriptMock.allActualArgs[i++])
assertEquals("git checkout myVersion", scriptMock.allActualArgs[i++])
assertEquals("git push origin main/1.0 dev/1.0 myVersion", scriptMock.allActualArgs[i++])
assertEquals("git push --delete origin myReleaseBranch", scriptMock.allActualArgs[i++])
}

@Test
void testFinishReleaseWithBaseVersionSet() {
String releaseBranchAuthorName = 'release'
String releaseBranchEmail = '[email protected]'
String releaseBranchAuthor = createGitAuthorString(releaseBranchAuthorName, releaseBranchEmail)
String developBranchAuthorName = 'develop'
String developBranchEmail = '[email protected]'
String developBranchAuthor = createGitAuthorString(developBranchAuthorName, developBranchEmail)
scriptMock.expectedShRetValueForScript.put('git --no-pager show -s --format=\'%an <%ae>\' HEAD',
[releaseBranchAuthor, releaseBranchAuthor, releaseBranchAuthor, releaseBranchAuthor,
// these two are the ones where the release branch author is stored:
releaseBranchAuthor, releaseBranchAuthor,
developBranchAuthor, developBranchAuthor
])
scriptMock.expectedShRetValueForScript.put('git push origin 4.2.2/main 4.2.2/develop myVersion', 0)

scriptMock.expectedDefaultShRetValue = ""
scriptMock.env.BRANCH_NAME = "myReleaseBranch"
scriptMock.expectedShRetValueForScript.put('grep -e "^BASE_VERSION=" Makefile | sed "s/BASE_VERSION=//g"'.toString(), "4.2.2".toString())

Git git = new Git(scriptMock)
Makefile makefile = new Makefile(scriptMock)
GitFlow gitflow = new GitFlow(scriptMock, git, makefile)
gitflow.finishRelease("myVersion", "4.2.2/main", "4.2.2/develop")

scriptMock.allActualArgs.removeAll("echo ")
scriptMock.allActualArgs.removeAll("git --no-pager show -s --format='%an <%ae>' HEAD")
int i = 0
assertEquals("git ls-remote origin refs/tags/myVersion", scriptMock.allActualArgs[i++])
assertEquals("git config 'remote.origin.fetch' '+refs/heads/*:refs/remotes/origin/*'", scriptMock.allActualArgs[i++])
assertEquals("git fetch --all", scriptMock.allActualArgs[i++])
assertEquals("grep -e \"^BASE_VERSION=\" Makefile | sed \"s/BASE_VERSION=//g\"", scriptMock.allActualArgs[i++])
assertEquals("git log origin/myReleaseBranch..origin/4.2.2/develop --oneline", scriptMock.allActualArgs[i++])
assertEquals("git checkout myReleaseBranch", scriptMock.allActualArgs[i++])
assertEquals("git reset --hard origin/myReleaseBranch", scriptMock.allActualArgs[i++])
assertEquals("git checkout 4.2.2/develop", scriptMock.allActualArgs[i++])
assertEquals("git reset --hard origin/4.2.2/develop", scriptMock.allActualArgs[i++])
assertEquals("git checkout 4.2.2/main", scriptMock.allActualArgs[i++])
assertEquals("git reset --hard origin/4.2.2/main", scriptMock.allActualArgs[i++])

// Author & Email 1 (calls 'git --no-pager...' twice)
assertEquals("git merge --no-ff myReleaseBranch", scriptMock.allActualArgs[i++])
assertAuthor(0, releaseBranchAuthorName, releaseBranchEmail)

// Author & Email 2 (calls 'git --no-pager...' twice)
assertEquals("git tag -f -m \"release version myVersion\" myVersion", scriptMock.allActualArgs[i++])
assertAuthor(1, releaseBranchAuthorName, releaseBranchEmail)

assertEquals("git checkout 4.2.2/develop", scriptMock.allActualArgs[i++])
// Author & Email 3 (calls 'git --no-pager...' twice)
assertEquals("git merge --no-ff myReleaseBranch", scriptMock.allActualArgs[i++])
assertAuthor(2, releaseBranchAuthorName, releaseBranchEmail)

assertEquals("git branch -d myReleaseBranch", scriptMock.allActualArgs[i++])
assertEquals("git checkout myVersion", scriptMock.allActualArgs[i++])
assertEquals("git push origin 4.2.2/main 4.2.2/develop myVersion", scriptMock.allActualArgs[i++])
assertEquals("git push --delete origin myReleaseBranch", scriptMock.allActualArgs[i++])
}

@Test
void testFinishReleaseWithBaseVersionUnSet() {
String releaseBranchAuthorName = 'release'
String releaseBranchEmail = '[email protected]'
String releaseBranchAuthor = createGitAuthorString(releaseBranchAuthorName, releaseBranchEmail)
String developBranchAuthorName = 'develop'
String developBranchEmail = '[email protected]'
String developBranchAuthor = createGitAuthorString(developBranchAuthorName, developBranchEmail)
scriptMock.expectedShRetValueForScript.put('git --no-pager show -s --format=\'%an <%ae>\' HEAD',
[releaseBranchAuthor, releaseBranchAuthor, releaseBranchAuthor, releaseBranchAuthor,
// these two are the ones where the release branch author is stored:
releaseBranchAuthor, releaseBranchAuthor,
developBranchAuthor, developBranchAuthor
])
scriptMock.expectedShRetValueForScript.put('git push origin master develop myVersion', 0)

scriptMock.expectedDefaultShRetValue = ""
scriptMock.env.BRANCH_NAME = "myReleaseBranch"
scriptMock.expectedShRetValueForScript.put('grep -e "^BASE_VERSION=" Makefile | sed "s/BASE_VERSION=//g"'.toString(), "".toString())

Git git = new Git(scriptMock)
Makefile makefile = new Makefile(scriptMock)
GitFlow gitflow = new GitFlow(scriptMock, git, makefile)
gitflow.finishRelease("myVersion")

scriptMock.allActualArgs.removeAll("echo ")
scriptMock.allActualArgs.removeAll("git --no-pager show -s --format='%an <%ae>' HEAD")
int i = 0
assertEquals("git ls-remote origin refs/tags/myVersion", scriptMock.allActualArgs[i++])
assertEquals("git config 'remote.origin.fetch' '+refs/heads/*:refs/remotes/origin/*'", scriptMock.allActualArgs[i++])
assertEquals("git fetch --all", scriptMock.allActualArgs[i++])
assertEquals("grep -e \"^BASE_VERSION=\" Makefile | sed \"s/BASE_VERSION=//g\"", scriptMock.allActualArgs[i++])
assertEquals("git log origin/myReleaseBranch..origin/develop --oneline", scriptMock.allActualArgs[i++])
assertEquals("git checkout myReleaseBranch", scriptMock.allActualArgs[i++])
assertEquals("git reset --hard origin/myReleaseBranch", scriptMock.allActualArgs[i++])
assertEquals("git checkout develop", scriptMock.allActualArgs[i++])
assertEquals("git reset --hard origin/develop", scriptMock.allActualArgs[i++])
assertEquals("git checkout master", scriptMock.allActualArgs[i++])
assertEquals("git reset --hard origin/master", scriptMock.allActualArgs[i++])

// Author & Email 1 (calls 'git --no-pager...' twice)
assertEquals("git merge --no-ff myReleaseBranch", scriptMock.allActualArgs[i++])
assertAuthor(0, releaseBranchAuthorName, releaseBranchEmail)

// Author & Email 2 (calls 'git --no-pager...' twice)
assertEquals("git tag -f -m \"release version myVersion\" myVersion", scriptMock.allActualArgs[i++])
assertAuthor(1, releaseBranchAuthorName, releaseBranchEmail)

assertEquals("git checkout develop", scriptMock.allActualArgs[i++])
// Author & Email 3 (calls 'git --no-pager...' twice)
assertEquals("git merge --no-ff myReleaseBranch", scriptMock.allActualArgs[i++])
assertAuthor(2, releaseBranchAuthorName, releaseBranchEmail)

assertEquals("git branch -d myReleaseBranch", scriptMock.allActualArgs[i++])
assertEquals("git checkout myVersion", scriptMock.allActualArgs[i++])
assertEquals("git push origin master develop myVersion", scriptMock.allActualArgs[i++])
assertEquals("git push --delete origin myReleaseBranch", scriptMock.allActualArgs[i++])
}

@Test
void testThrowsErrorWhenTagAlreadyExists() {
scriptMock.expectedShRetValueForScript.put('git ls-remote origin refs/tags/myVersion', 'thisIsATag')
Expand All @@ -163,6 +336,55 @@ class GitFlowTest {
assertEquals("There are changes on develop branch that are not merged into release. Please merge and restart process.", err.getMessage())
}

@Test
void testIsUnallowedBackportReleaseIsUnallowedStandardBranches() {
scriptMock.expectedShRetValueForScript.put('grep -e "^BASE_VERSION=" Makefile | sed "s/BASE_VERSION=//g"'.toString(), "4.2".toString())

Git git = new Git(scriptMock)
Makefile makefile = new Makefile(scriptMock)
GitFlow gitflow = new GitFlow(scriptMock, git, makefile)
def result = gitflow.isUnallowedBackportRelease("main", "develop")

assertEquals(true, result)
}

@Test
void testIsUnallowedBackportReleaseIsUnallowedStandardBranches2() {
scriptMock.expectedShRetValueForScript.put('grep -e "^BASE_VERSION=" Makefile | sed "s/BASE_VERSION=//g"'.toString(), "4.2".toString())

Git git = new Git(scriptMock)
Makefile makefile = new Makefile(scriptMock)
GitFlow gitflow = new GitFlow(scriptMock, git, makefile)
def result = gitflow.isUnallowedBackportRelease("master", "develop")

assertEquals(true, result)
}

@Test
void testIsUnallowedBackportReleaseIsUnallowedWrongVersionBranches() {
scriptMock.expectedShRetValueForScript.put('grep -e "^BASE_VERSION=" Makefile | sed "s/BASE_VERSION=//g"'.toString(), "4.2".toString())

Git git = new Git(scriptMock)
Makefile makefile = new Makefile(scriptMock)
GitFlow gitflow = new GitFlow(scriptMock, git, makefile)
def result = gitflow.isUnallowedBackportRelease("4.3/main", "4.3/develop")

assertEquals(true, result)
}

@Test
void testIsUnallowedBackportReleaseIsAllowed() {
scriptMock.expectedShRetValueForScript.put('grep -e "^BASE_VERSION=" Makefile | sed "s/BASE_VERSION=//g"'.toString(), "4.2".toString())

Git git = new Git(scriptMock)
Makefile makefile = new Makefile(scriptMock)
GitFlow gitflow = new GitFlow(scriptMock, git, makefile)
def result = gitflow.isUnallowedBackportRelease("4.2/main", "4.2/develop")

assertEquals(false, result)
}


void assertAuthor(int withEnvInvocationIndex, String author, String email) {
def withEnvMap = scriptMock.actualWithEnvAsMap(withEnvInvocationIndex)
assert withEnvMap['GIT_AUTHOR_NAME'] == author
Expand Down
Loading