Skip to content

Commit

Permalink
Merge pull request #46 from cloudogu/feature/areChangesStagedForCommit
Browse files Browse the repository at this point in the history
Extensions to Git and Docker
  • Loading branch information
marekzan authored Aug 27, 2020
2 parents 09e5d4e + 6b1c644 commit 0da10b8
Show file tree
Hide file tree
Showing 7 changed files with 178 additions and 14 deletions.
6 changes: 6 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -473,6 +473,7 @@ gitWithCreds 'https://your.repo' // Implicitly passed credentials
* `git.commitMessage` - Last commit message e.g. `Implements new functionality...`
* `git.commitHash` - e.g. `fb1c8820df462272011bca5fddbe6933e91d69ed`
* `git.commitHashShort` - e.g. `fb1c882`
* `git.areChangesStagedForCommit()` - `true` if changes are staged for commit. If `false`, `git.commit()` will fail.
* `git.repositoryUrl` - e.g. `https://github.com/orga/repo.git`
* `git.gitHubRepositoryName` - e.g. `orga/repo`
* Tags - Note that the git plugin might not fetch tags for all builds. Run `sh "git fetch --tags"` to make sure.
Expand Down Expand Up @@ -598,6 +599,11 @@ Example from Jenkinsfile:

## Additional features provided by the `Docker.Image` class

* `repoDigests()`: Returns the repo digests, a content addressable unique digest of an image that was pushed
to or pulled from repositories.
If the image was built locally and not pushed, returns an empty list.
If the image was pulled from or pushed to a repo, returns a list containing one item.
If the image was pulled from or pushed to multiple repos, might also contain more than one digest.
* `mountJenkinsUser()`: Setting this to `true` provides the user that executes the build within docker container's `/etc/passwd`.
This is necessary for some commands such as npm, ansible, git, id, etc. Those might exit with errors withouta user
present.
Expand Down
38 changes: 35 additions & 3 deletions src/com/cloudogu/ces/cesbuildlib/Docker.groovy
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ class Docker implements Serializable {
String findIp(container) {
sh.returnStdOut "docker inspect -f '{{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}' ${container.id}"
}

/**
* @return the IP address in the current context: the docker host ip (when outside of a container) or the ip of the
* container this is running in
Expand Down Expand Up @@ -235,17 +235,33 @@ class Docker implements Serializable {
* Runs docker tag to record a tag of this image (defaulting to the tag it already has). Will rewrite an
* existing tag if one exists.
*/
void tag(String tagName = image().parsedId.tag, boolean force = true) {
void tag(String tagName, boolean force) {
image().tag(tagName, force)
}

void tag(String tagName) {
image().tag(tagName)
}

void tag() {
image().tag()
}

/**
* Pushes an image to the registry after tagging it as with the tag method. For example, you can use image().push
* 'latest' to publish it as the latest version in its repository.
*/
void push(String tagName = image().parsedId.tag, boolean force = true) {
void push(String tagName, boolean force) {
image().push(tagName, force)
}

void push(String tagName) {
image().push(tagName)
}

void push() {
image().push()
}

/**
* Provides the user that executes the build within docker container's /etc/passwd.
Expand Down Expand Up @@ -290,6 +306,22 @@ class Docker implements Serializable {
return this
}

/**
* Returns the repo digests, a content addressable unique digest of an image that was pushed to or pulled from
* a repository.
*
* @return If the image was built locally and not pushed, returns an empty list.
* If the image was pulled from or pushed to a repo, returns a list containing one item.
* If the image was pulled from or pushed to multiple repos, might also contain more than one digest.
*/
List repoDigests() {
def split = sh.returnStdOut(
"docker image inspect ${imageIdString} -f '{{range .RepoDigests}}{{printf \"%s\\n\" .}}{{end}}'")
.split('\n')
// Remove empty lines, e.g. the superflous last linebreak
return split - ''
}

private extendArgs(String args) {
String extendedArgs = args
if (mountJenkinsUser) {
Expand Down
17 changes: 17 additions & 0 deletions src/com/cloudogu/ces/cesbuildlib/Git.groovy
Original file line number Diff line number Diff line change
Expand Up @@ -197,6 +197,23 @@ class Git implements Serializable {
}
}

/**
* @return true when changes are staged for commit, i.e. "git add" detected changes.
* Note that this will not work on a branch which has no commits, e.g. newly initialized repositories.
*/
boolean areChangesStagedForCommit() {
// See https://stackoverflow.com/a/3879077/

// '--' at the end avoids matching a file called "HEAD"
String returnCode = script.sh(returnStatus: true, script: 'git update-index --refresh && git diff-index --exit-code HEAD --')
// --exit-code: exits with 1 if there were differences and 0 means no differences.
if (returnCode.equals("0")) {
return false
} else {
true
}
}

/**
* Sets Tag with message using the name and email of the last committer as author and committer.
*
Expand Down
12 changes: 10 additions & 2 deletions src/com/cloudogu/ces/cesbuildlib/SonarQube.groovy
Original file line number Diff line number Diff line change
Expand Up @@ -136,11 +136,19 @@ class SonarQube implements Serializable {
// CHANGE_TARGET=develop

def artifactId = mvn.artifactId.trim()
mvn.additionalArgs += " -Dsonar.projectKey=${mvn.groupId}:${artifactId}:${script.env.BRANCH_NAME}" +
" -Dsonar.projectName=${artifactId}:${script.env.BRANCH_NAME} "
// Malformed key for Project: 'groupId:artifactId:feature/something'.
// Allowed characters are alphanumeric, '-', '_', '.' and ':', with at least one non-digit.
String projectKey = replaceCharactersNotAllowedInProjectKey(script.env.BRANCH_NAME)
String projectName = script.env.BRANCH_NAME
mvn.additionalArgs += " -Dsonar.projectKey=${mvn.groupId}:${artifactId}:${projectKey}" +
" -Dsonar.projectName=${artifactId}:${projectName} "
}
}

protected String replaceCharactersNotAllowedInProjectKey(String potentialProjectKey) {
return potentialProjectKey.replaceAll("[^a-zA-Z0-9-_.:]", "_");
}

protected String determineIntegrationBranch() {
if (config['integrationBranch']) {
return config['integrationBranch']
Expand Down
75 changes: 73 additions & 2 deletions test/com/cloudogu/ces/cesbuildlib/DockerTest.groovy
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,9 @@ class DockerTest {
def actualDockerGroup = "docker:x:$actualDockerGroupId:jenkins"
Map<String, String> actualWriteFileArgs = [:]
def actualShArgs = new LinkedList<Object>()
def actualRepoDigests = ''
def actualPushParams
def actualTagParams

@Test
void findIpOfContainer() {
Expand Down Expand Up @@ -394,6 +397,71 @@ class DockerTest {
assert actualShArgs[0].contains("${actualDockerServerVersion}.tgz")
}

@Test
void "repo digest"() {
def expectedDigest = "hello-world@sha256:7f0a9f93b4aa3022c3a4c147a449bf11e0941a1fd0bf4a8e6c9408b2600777c5"
actualRepoDigests = expectedDigest + "\n\n"
def digests = createWithImage().image(expectedImage).repoDigests()

assert digests.size() == 1
assert digests[0] == expectedDigest
}

@Test
void "repo digest empty"() {
actualRepoDigests = '\n'
def digests = createWithImage().image(expectedImage).repoDigests()

assert digests.size() == 0
}

@Test
void "repo digest multiple"() {
actualRepoDigests = "a\nb\nc\n\n"
def digests = createWithImage().image(expectedImage).repoDigests()

assert digests.size() == 3
assert digests[0] == 'a'
assert digests[1] == 'b'
assert digests[2] == 'c'
}

@Test
void "push image"() {
createWithImage().image(expectedImage).push()
assert actualPushParams == ['', null]
}

@Test
void "push image with name"() {
createWithImage().image(expectedImage).push('name')
assert actualPushParams == ['name', null]
}

@Test
void "push image with name and force"() {
createWithImage().image(expectedImage).push('name', true)
assert actualPushParams == ['name', true]
}

@Test
void "tag image"() {
createWithImage().image(expectedImage).tag()
assert actualTagParams == ['', null]
}

@Test
void "tag image with name"() {
createWithImage().image(expectedImage).tag('name')
assert actualTagParams == ['name', null]
}

@Test
void "tag image with name and force"() {
createWithImage().image(expectedImage).tag('name', true)
assert actualTagParams == ['name', true]
}

private Docker create(Map<String, Closure> mockedMethod) {
Map<String, Map<String, Closure>> mockedScript = [
docker: mockedMethod
Expand All @@ -405,11 +473,13 @@ class DockerTest {
* @return Mock Docker instance with mock image, that contains mocked methods.
*/
@SuppressWarnings("GroovyAssignabilityCheck")
private Docker createWithImage(Map<String, Closure> mockedMethod) {
private Docker createWithImage(Map<String, Closure> mockedMethod = [:]) {
def mockedScript = [
docker: [image: { String id ->
assert id == expectedImage
mockedMethod.put('id', id)
mockedMethod.put('push', { String param1 = '', Boolean param2 = null -> actualPushParams = [param1, param2] })
mockedMethod.put('tag', { String param1 = '', Boolean param2 = null -> actualTagParams = [param1, param2] })
return mockedMethod
}
],
Expand All @@ -429,6 +499,7 @@ class DockerTest {
if (script.contains(actualDockerGroup)) return actualDockerGroupId
if (script.contains('cat /etc/passwd | grep')) return actualPasswd
if (script.contains('docker version --format \'{{.Server.Version}}\'')) return " ${actualDockerServerVersion} "
if (script.contains('RepoDigests')) return " ${actualRepoDigests} "
else fail("Unexpected sh call. Script: " + script)
},
pwd: { return expectedHome },
Expand All @@ -452,7 +523,7 @@ class DockerTest {
private def mockedImageMethodInside() {
[inside: { String param1, Closure param2 -> return [param1, param2] }]
}

/**
* @return a map that defines a withRun() method returning its params, to be used as param in createWithImage()}.
*/
Expand Down
12 changes: 12 additions & 0 deletions test/com/cloudogu/ces/cesbuildlib/GitTest.groovy
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,18 @@ class GitTest {
assertEquals(expectedReturnValue, git.commitHashShort)
}

@Test
void changesStagedForCommit() {
scriptMock.expectedDefaultShRetValue = 1
assertTrue git.areChangesStagedForCommit()
}

@Test
void noChangesStagedForCommit() {
scriptMock.expectedDefaultShRetValue = 0
assertFalse git.areChangesStagedForCommit()
}

@Test
void getRepositoryUrl() {
String expectedReturnValue = "https://github.com/orga/repo.git"
Expand Down
32 changes: 25 additions & 7 deletions test/com/cloudogu/ces/cesbuildlib/SonarQubeTest.groovy
Original file line number Diff line number Diff line change
Expand Up @@ -37,16 +37,17 @@ class SonarQubeTest {
void analyzeWithToken() throws Exception {
def sonarQube = new SonarQube(scriptMock, [token: 'secretTextCred', sonarHostUrl: 'http://ces/sonar'])

def branchName = 'develop.Or:somehing-completely_.different'
scriptMock.env = [
SONAR_AUTH_TOKEN: 'auth',
BRANCH_NAME : 'develop'
BRANCH_NAME : branchName
]

sonarQube.analyzeWith(mavenMock)

assert mavenMock.args ==
'sonar:sonar -Dsonar.host.url=http://ces/sonar -Dsonar.login=auth '
assertBranchName()
assertBranchName(branchName, branchName)
assert scriptMock.actualStringArgs['credentialsId'] == 'secretTextCred'
}

Expand Down Expand Up @@ -74,7 +75,7 @@ class SonarQubeTest {

assert mavenMock.args ==
'sonar:sonar -Dsonar.host.url=http://ces/sonar -Dsonar.login=usr -Dsonar.password=pw '
assertBranchName()
assertBranchName('develop', 'develop')
assert scriptMock.actualUsernamePasswordArgs[0]['credentialsId'] == 'usrPwCred'
}

Expand Down Expand Up @@ -127,7 +128,7 @@ class SonarQubeTest {

assert mavenMock.args ==
'sonar:sonar -Dsonar.host.url=host -Dsonar.login=auth -DextraKey=extraValue'
assertBranchName()
assertBranchName('develop', 'develop')
assert scriptMock.actualSonarQubeEnv == 'sqEnv'
}

Expand All @@ -152,6 +153,23 @@ class SonarQubeTest {

assert mavenMock.additionalArgs == ''
}

@Test
void analyzeWithBranchContainCharsNotValidForProjectKey() throws Exception {
String branchName = 'feature/abc'
String projectKey = 'feature_abc'
String projectName = branchName

def sonarQube = new SonarQube(scriptMock, [usernamePassword: 'usrPwCred', sonarHostUrl: 'http://ces/sonar'])
scriptMock.env = [
SONAR_AUTH_TOKEN: 'auth',
BRANCH_NAME : branchName
]

sonarQube.analyzeWith(mavenMock)

assertBranchName(projectKey, projectName)
}

@Test
void analyzeWithBranchPlugin() throws Exception {
Expand Down Expand Up @@ -303,7 +321,7 @@ class SonarQubeTest {
assert exception.message == "Missing required 'sonarHostUrl' parameter."
}

void assertBranchName() {
assert mavenMock.additionalArgs.contains("-Dsonar.projectKey=com.cloudogu.ces:ces-build-lib:develop -Dsonar.projectName=ces-build-lib:develop ")
void assertBranchName(String projectKey, String projectName) {
assert mavenMock.additionalArgs.contains("-Dsonar.projectKey=com.cloudogu.ces:ces-build-lib:${projectKey} -Dsonar.projectName=ces-build-lib:${projectName}")
}
}
}

0 comments on commit 0da10b8

Please sign in to comment.