Skip to content

Instantly share code, notes, and snippets.

@gestj
Last active December 16, 2015 18:57
Show Gist options
  • Save gestj/c6a9b948b422ec82f1c6 to your computer and use it in GitHub Desktop.
Save gestj/c6a9b948b422ec82f1c6 to your computer and use it in GitHub Desktop.
Script for jenkins scriptler plugin (works with Jenkins v1.572)
import groovy.json.*
import hudson.model.*
import static groovy.json.JsonOutput.*
import java.util.logging.*
import java.security.MessageDigest
class Bitbucket {
def static BITBUCKET_REST_URL = "https://bitbucket.org"
def static BITBUCKET_API_URL = BITBUCKET_REST_URL + "/api/2.0/"
def static LOGGER = Logger.getLogger("connect.bitbucket.commit.groovy.Bitbucket")
def user, pass
Bitbucket(user, pass) {
this.user = user
this.pass = pass
}
def post(resource, map) {
LOGGER.info("POST -> ${BITBUCKET_API_URL}${resource}\n data: " + map)
def conn = connect(resource)
conn.setDoOutput(true)
conn.setRequestMethod("POST")
def out = new OutputStreamWriter(conn.getOutputStream());
out.write(toJson(map))
out.close()
return getResponse(conn)
}
def connect(resource) {
def conn = "${BITBUCKET_API_URL}${resource}".toURL().openConnection()
def auth = "${user}:${pass}".getBytes().encodeBase64().toString()
conn.setRequestProperty("Authorization", "Basic ${auth}")
conn.setRequestProperty("content-type", "application/json; charset=utf-8");
return conn
}
def getResponse(conn) {
if (conn.responseCode == 200 || conn.responseCode == 201) {
return new JsonSlurper().parseText(conn.content.text)
} else if (conn.responseCode == 400) {
throw new IllegalArgumentException(formatError("Bad request", conn))
} else if (conn.responseCode == 404) {
throw new IllegalArgumentException(formatError("Resource not found", conn))
} else if (conn.responseCode == 401) {
throw new SecurityException(formatError("Not authorized", conn))
} else {
throw new RuntimeException(formatError("Unexpected error", conn))
}
}
def formatError(msg, conn) {
LOGGER.log(Level.WARNING, "${msg} (${conn.responseCode} - ${conn.responseMessage}):\n${conn.errorStream?.text}")
"${msg} (${conn.responseCode} - ${conn.responseMessage})"
}
// I added retry because sometimes Bitbucket fails to update the build status..
// => I want to be sure that a once started (inprogress) build gets its final result properly updated.
/**
* Will retry given func up to 3 times (catches thrown exceptions, sleeps and retries).
* @param func the function you wanna execute
* @param times handled by _retry itself - don't use it
* @return the return value of func if it has some and only if successful
*/
def _retry(Closure func, times=1) {
def response = false
try {
response = func()
} catch(e) {
// "exception" is already logged
if (times < 3) {
LOGGER.fine("Will try again after waiting a little...")
sleep(2000 * times)
_retry(func, times + 1)
} else {
LOGGER.log(Level.WARNING, "Tried 3 times to execute your function, but didn't succeed.")
}
}
return response
}
/**
* Updates build status of a commit in Bitbucket.
*
* If post to Bitbucket fails, it'll be tried again after a short amount of time (up to 3 times).
* If still unsuccessful Bitbucket stays untouched. Method then will return false.
*
* @param account account of the repository in Bitbucket
* @param repo the targeted repository
* @param revision the revision where the build status shall be set
* @param state the status of the corresponding build
* @param key the key of the corresponding build
* @param name the name of the corresponding build
* @param url the url to the corresponding build
* @param description this is optional - you can just leave it or put a string there
* @return response of Bitbucket
*
* @see https://confluence.atlassian.com/bitbucket/statuses-build-resource-779295267.html
*/
def updateBuildStatus(account, repo, revision, state, key, name, url, description="") {
return _retry({
post("repositories/${account}/${repo}/commit/${revision}/statuses/build", [
state: state,
key: key,
name: name,
url: url,
description: description
])
})
}
}
def logger = Logger.getLogger("connect.bitbucket.commit.groovy")
def thr = Thread.currentThread();
def currentBuild = thr?.executable
def env = currentBuild.getEnvironment(listener)
def url = currentBuild.url
def _id(env) {
return MessageDigest.getInstance("MD5").digest(env.BUILD_TAG.bytes).encodeHex().toString()
}
def _name(env) {
def branch = env.GIT_BRANCH
if (branch != null) {
def name = "unknown"
if (branch.startsWith("origin/")) {
name = branch.split("origin/")[1]
} else if (branch.equals("detached")) {
name = "release"
} else if (branch.startsWith("refs/tags")) {
name = "promote"
}
return name + " (#" + env.BUILD_NUMBER + ")"
}
return "#" + env.BUILD_NUMBER
}
def _repo(env) {
if (repo.equals("_env_")) {
return env.GIT_REPOSITORY
}
return repo
}
def _state(build) {
if (build.result == null) {
return "INPROGRESS"
} else if (build.result.isBetterOrEqualTo(Result.SUCCESS) || build.result == Result.NOT_BUILT) {
return "SUCCESSFUL"
}
return "FAILED"
}
def _bitbucket(env, repo, sha1, state, id, name, url) {
logger.fine("update ${sha1}")
Bitbucket(env.BITBUCKET_USER, env.BITBUCKET_PASS).updateBuildStatus(
env.BITBUCKET_TEAM, repo, sha1, state, id, name,
env.JENKINS_BASE_URL + "/${url}")
}
// updates for each commit connected to this build or if no change just takes the last commit
if (!build.getChangeSet().isEmptySet()) {
build.getChangeSet().getLogs().each {
def sha = it.id
_bitbucket(_repo(env), sha, _state(currentBuild), _id(env), _name(env), url)
}
} else {
def sha = build.getAction(hudson.plugins.git.util.BuildData).getLastBuiltRevision().sha1.name()
_bitbucket(_repo(env), sha, _state(currentBuild), _id(env), _name(env), url)
}
return "connect.bitbucket.commit.groovy done"
@gestj
Copy link
Author

gestj commented Dec 1, 2015

Why? For what?

This uses the just updated Bitbucket API to add a "build status" to your commits.

See http://blog.bitbucket.org/2015/11/18/introducing-the-build-status-api-for-bitbucket-cloud/

Usage

About the code

The class Bitbucket comes from a small groovy lib we wrote for our jenkins. It's more complex than needed for this specific purpose, but it works...

Interesting part starts at line 115 where build env gets collected, some helper methods are defined and finally the if else statement. It intelligently searches for the right commits associated to the current build.

(i) Also keep in mind: some parts are specific to the project I am using this. For example:

def _name(env) {
    ...
    } else if (branch.equals("detached")) {
        name = "release"
    } else if (branch.startsWith("refs/tags")) {
        name = "promote" 
    }
    ...
}

Check out / change also (in the version posted you need to provide repo as parameter):

def _repo(env) {
    if (repo.equals("_env_")) {
        return env.GIT_REPOSITORY
    }
    return repo
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment