You can now vote for scripts to help people know if they're useful or not. Login or create an account to vote!


Custom Bitbucket Commit Message Validation


When pushing a commit, this custom pre-hook requires users to begin the commit message with a Jira issue key, in the format "[JIRA-ID]". The Jira issue key helps create an audit trail between stories and build deployments. This particular custom script also allows for some exceptions like commit messages starting with "[EMERGENCY]" or "Revert" or commits created by service accounts.

This script was created and provided by GAIN Capital.


I have a code branch with changes ready to push to Bitbucket. To create an audit trail and to avoid an error message, I add a Jira issue key to the beginning of the commit message.

Good to Know

  • If a Jira issue key is not added to a commit message, the ScriptRunner for Bitbucket pre-hook rejects the push with a custom error message.
  • The revert commits must be written on three lines. The first line must contain the word "Revert" and the third line must contain the words "This reverts commit".


  • Bitbucket Bitbucket (6.3 - 7.12)
  • ScriptRunner for Bitbucket ScriptRunner for Bitbucket (6.11.0)
/** * Copyright © 2020, StoneX/Gain Capital * * This script is released under the BSD 3-clause license: * * Pre-hook checks if the message begins with ticket key in order to achieve better linkage of stories, builds, * deployments and change request tickets through automated pipelines and improved audit. */ boolean isSquashedCommit(String[] commitMessage) { // We only handle squashed commits done via Bitbucket when merging PR // For local squashed merge, the user is expected to add [JIRA-ID] def isSquashedCommit = false def isPrMerge = (commitMessage[0] =~ /^Merge pull request #\d+ in.*/).find() if (isPrMerge) { def isSpquashedCommit = (commitMessage[2] =~ /^Squashed commit of the following:$/).find() if (isSpquashedCommit) { // We check commits in the squashed commit. This is just additional check to deter // spoofing and there should be at least one commit ID commitMessage.any { line -> // E.g. commit 3414e31e819bcff53b8c9e4387c1eaec840a294e // Commit hash is always length of 40 def commitIdFound = (line =~ /^commit [A-Za-z0-9]{40,40}/).find() if (commitIdFound) { isSquashedCommit = true return true // break 'any' closure } } } } isSquashedCommit } try { def success = true refChanges.each { refChange -> refChange.getChangesets(repository).each { changeset -> def commit = changeset.toCommit def commitMessage = changeset.toCommit.message def splitMessage = commitMessage.split('\n') def committer = def author = // The names of all service accounts begin with "svc." def isCommitterSvc = (committer =~ /^svc.*/).find() def isAuthorSvc = (author =~ /^svc.*/).find() //Merge commits have two parents. If size is greater than 0 then it means its a merge commit def isItMergeCommit = commit.parents.size() > 1 def isItRevertCommit = false def isSquashedCommit = (isSquashedCommit(splitMessage) && commit.parents.size() == 1) if (commitMessage.startsWith('Revert')) { isItRevertCommit = (splitMessage.size() >= 3 && splitMessage[0] =~ /^Revert/).find() && (splitMessage[2] =~ /^This reverts commit/).find() } if (!commitMessage) { // This should not happen as we have commit message control so we simply log and ignore log.warn("No commit message in ${commit.displayId}") } else if (!isItMergeCommit && !isItRevertCommit && !isCommitterSvc && !isAuthorSvc && !isSquashedCommit) { // Message have to begin with [JIRA-ID] or "[EMERGENCY]" word def issueMatcher = commitMessage =~ /^\s*\[+[A-Z0-9]+-[0-9]+\]+|^\s*\[EMERGENCY\]/ def isIssueInMessage = issueMatcher.find() if (!isIssueInMessage) { hookResponse.out().println("Commit " + commit.displayId + " message must contain Jira ticket ID associated with this commit." + " https://.../display/ABC/Commit+JIRA+id+control") success = false } } } } return success } catch (Exception ex) { log.error("Exception: ${ex}") return false }
Discovered an issue? Report it here

Suggested for you