Custom Bitbucket Commit Message Validation

Overview

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.

Example

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".

Requirements

Bitbucket Bitbucket (5.6 - 7.6)

    
/** * Copyright © 2020, StoneX/Gain Capital * * This script is released under the BSD 3-clause license: https://opensource.org/licenses/BSD-3-Clause * * 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 = changeset.toCommit.committer.name.toLowerCase() def author = changeset.toCommit.author.name.toLowerCase() // 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