GitLab and Jira are widely recognized as powerful tools, extensively used by teams across various industries, including Scrum Masters, team members, and project managers.
However, one significant challenge in using these tools is integrating them to synchronize tasks with the actual code written by developers. This integration allows teams to monitor the work process, track work items in releases, and determine what has been completed versus what is still under development. Moreover, it enables the creation of meaningful reports on tasks and the product's feature set.
The simplest solution is the GitLab and Jira integration app provided by Jira, which can be installed via Atlassian's Marketplace. This app enables you to link commits and branch work directly to Jira issues by including the issue ID in the branch name or merge request title. It helps teams track progress in Jira efficiently. For more details, you can refer to this resource.
But what if we need more advanced integration? For instance, what if we want to automatically update the status of Jira items to "Released" after merging a branch into the production branch? Or control the ability to merge into the main branch based on the Jira item's state? Additionally, what if we need to generate reports detailing which tasks were completed between two specific releases or revisions? These scenarios require deeper customization and tighter integration between GitLab and Jira to align development workflows with project management needs.
Recently, I worked on a similar challenge. The Scrum Team wanted to control the state of items being merged into the main branch, ensuring that only items marked as "Accepted" by the product team after acceptance testing could be merged. Additionally, once merged, the items' status would automatically update to "Released." Since both GitLab and Jira offer robust and powerful APIs, we leveraged them to implement a solution that met these requirements.
The first step is to get notified about GitLab merge requests so we can track their creation and state changes. GitLab provides a Webhooks feature, which sends requests to external services whenever specific events occur in your project, such as code pushes, merge request updates, or comment changes—essentially, almost anything that happens in your repository. Webhooks allow us to stay updated on repository activity, but what if we want more control over the repository itself? For example, in my case, I needed to block merge requests with invalid Jira states to ensure only valid items could proceed.
To gain control over your project's merge requests, I explored two different approaches. The first approach involves creating a dedicated GitLab user and adding it as a merge request approver in your project. You can then use your code to programmatically manage the approvals, allowing you to enforce custom rules and conditions for merging.
The second approach takes advantage of GitLab’s super handy External Status Check feature. With this, GitLab sends your project details to a custom API you create, and your API responds with one of three states: "passed," "pending," or "failed." Based on these responses, you can easily block merge requests that don’t meet your conditions.
to use external status checks you can access it through Project Setting> Merge request > Merge Checks.
activate the "Status checks must succeed" option and you can add new status check.
Click on Add status check, give it a name, and add your API URL endpoint. Now, for each merge request, GitLab will send a request to your endpoint with the following content:
Now that we have the data about the merge request, the next step is to process this information and update the status check state accordingly.
I used Golang to implement an HTTP server that receives GitLab status check requests.
package main
import (
"fmt"
"net/http"
"github.com/gin-gonic/gin"
)
type GitLabStatusCheckRequest struct {
ObjectKind string `json:"object_kind"`
ObjectAttributes struct {
Title string `json:"title"`
} `json:"object_attributes"`
}
func main() {
r := gin.Default()
// Endpoint to handle GitLab External Status Check requests
r.POST("/gitlab/status-check", func(c *gin.Context) {
var req GitLabStatusCheckRequest
// Parse the incoming JSON request
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid request format"})
return
}
// Check if the request is for a merge request
if req.ObjectKind != "merge_request" {
c.JSON(http.StatusBadRequest, gin.H{"error": "Only merge request events are supported"})
return
}
// Print the merge request title
fmt.Printf("Merge Request Title: %s\n", req.ObjectAttributes.Title)
// Respond with success
c.JSON(http.StatusOK, gin.H{"status": "Merge request received successfully"})
})
// Start the server on port 8080
if err := r.Run(":8080"); err != nil {
fmt.Printf("Failed to start server: %v\n", err)
}
}
Now that we have the merge request data in our service and the merge request is blocked (thanks to the status check), the next step is to validate the Jira items in the merge request title. Based on the validation, we'll set the status check to either "failed" or "passed."
We need to extract the Jira item IDs from the merge request title. I wrote this function to match the Jira IDs in the title:
func extractJiraIDs(title string) []string {
// Regular expression to match Jira item IDs (e.g., ABC-123)
re := regexp.MustCompile(`[A-Z]+-\d+`)
return re.FindAllString(title, -1)
}
Next, I wrote following function to fetch the Jira item states using Jira’s REST API. You can check the API reference here
func fetchJiraStatus(jiraID string) (string, error) {
url := fmt.Sprintf("%s/rest/api/3/issue/%s", JiraBaseURL, jiraID)
// Create HTTP request
req, err := http.NewRequest("GET", url, nil)
if err != nil {
return "", err
}
req.SetBasicAuth(JiraUsername, JiraAPIToken)
req.Header.Set("Accept", "application/json")
// Make HTTP request
client := &http.Client{}
resp, err := client.Do(req)
if err != nil {
return "", err
}
defer resp.Body.Close()
// Handle non-200 responses
if resp.StatusCode != http.StatusOK {
return "", fmt.Errorf("failed to fetch Jira issue %s: %s", jiraID, resp.Status)
}
// Parse response body
body, err := ioutil.ReadAll(resp.Body)
if err != nil {
return "", err
}
var issue JiraIssue
if err := json.Unmarshal(body, &issue); err != nil {
return "", err
}
return issue.Fields.Status.Name, nil
}
Now that we have the Jira items and their states, we need to fail the merge request status check if any item has a state other than "Accepted." To achieve this, I wrote a function that sets the status check state using the GitLab REST API. You can find the documentation for this API here
func setGitLabStatus(projectID, mergeRequestID int, status string) error {
url := fmt.Sprintf("%s/projects/%d/merge_requests/%d/status_checks", GitLabBaseURL, projectID, mergeRequestID)
payload := map[string]string{"status": status}
data, err := json.Marshal(payload)
if err != nil {
return err
}
req, err := http.NewRequest("PATCH", url, bytes.NewBuffer(data))
if err != nil {
return err
}
req.Header.Set("Private-Token", GitLabToken)
req.Header.Set("Content-Type", "application/json")
client := &http.Client{}
resp, err := client.Do(req)
if err != nil {
return err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
body, _ := ioutil.ReadAll(resp.Body)
return fmt.Errorf("failed to set GitLab status: %s", string(body))
}
return nil
}
Congratulations, we’re done! Depending on the status check state, you’ll now see one of two outcomes based on the conditions we’ve set.
Based on your requirements, you can do a lot with the GitLab and Jira APIs. For example, you could change Jira item states when a merge is completed to keep track of tasks completed on the main branch. You can also create multiple rules for merging into dev, main, or feature branches, giving you full control over your tasks across different branches.
Or maybe you want to perform some static or dynamic checks on your code outside of your GitLab runners? You can use status checks to achieve that.
GitLab webhooks were a huge help in syncing and integrating Jira with the actual code work, allowing me to create meaningful and live reports on the product development state. These are powerful tools, and there’s so much you can do with them—this is just one of the use cases I encountered. Feel free to reach out if you need more information, or I’d love to hear your thoughts on this post. You can find the full code snippet on this [Gist]. Thanks for taking the time to read, and I hope this was helpful!
Resources
https://docs.gitlab.com/ee/user/project/merge_requests/status_checks.html
https://developer.atlassian.com/cloud/jira/platform/rest/v2/api-group-issues/