Python integration of Gitlab, Jira and Confluence to automate release builds

Recently, at a stand-up, a colleague made a rational proposal : to automate the release build, taking as a basis ready-made already practices for interacting with Jira written in Python.

The deployment process is as follows: when a sufficient number of tasks that have been tested accumulate, they release a candidate candidate (RC) in each project affected by the tasks, then the tasks are tested as part of RC. After that, the RC is poured onto the staging server, where, in close proximity to the combat environment, it is still tested and a full regression is carried out. And then, after the necessary deployment actions, the fresh release is poured into the master.

Until recently, the entire assembly process was carried out manually by one of the developers. Which took an hour, two or more time and was, it seems to me, not a very interesting occupation. Now, when almost everything is ready, the release of 20 tasks, affecting 5 projects, is going to less than a minute. There remains, of course, conflict resolution, running missed tests, and more, but even taking into account this, the time of developers and testers, who have to wait until someone is the first to free themselves and create RC, is saved a lot.

In general, I set about the task, and it turned out to be very interesting and fascinating. And what else is needed for the pleasure of work, if not exciting projects?

I studied legacy: it turned out to use the Jira API directly, and it was written, it seemed to me, not optimal. For example, the list of release tasks was obtained as follows: all existing releases were downloaded from Jira, and then each of them by name was compared with the name of our release until the desired one was found:

def get_release_info(config):

   try:

       release_input = sys.argv[1]

   except IndexError:

       raise Exception('Enter release name')

   releases_json = requests.get(url=RELEASES_LIST_URL, auth=(login, jira_password).json()

   for release in releases_json:

       if release['name'] == release_input:

                ...


In general, direct interaction with the API leads to not very readable code. And I didn’t want to invent a bicycle. My first search on Github led me to the JIRA Python Library, a fairly simple and powerful library. I initially planned to create Marge Requests using the GitPython library, also found on Github. But further study of the issue immediately led to the idea of ​​finding something related not to git, but rather immediately to Gitlab. As a result, I settled on the most famous solution: Python GitLab.

I started by getting a list of tasks in the appropriate status for the release . I decided not to strongly alter the previous solution, but to make it more effective by immediately requesting the API of the task of the desired release:

fix_issues = jira.search_issues(f'fixVersion={release_input}')

fix_id = jira.issue(fix_issues.iterable[0]).fields.fixVersions[0].id

Although most likely the same thing happens under the hood, however, it turned out to be more beautiful and optimal, but still not very readable. Further, from the received tasks, it is necessary to collect links to merge requests. I decided to store the found merge requests in namedtuple, they are great for this:

Merge_request = namedtuple('Merge_request', ['url', 'iid', 'project', 'issue'])

Merge requests were also received using the Jira API:

projects = set()

links_json = requests.get(url=REMOTE_LINK.format(issue_number),

                            auth=login,jira_password).json()

for link in links_json:

   url_parts = link['object']['url'].split('/')

   project = f'{url_parts[4]}'

   iid = url_parts[6]

   projects.add(project)   

After that, I decided where I could use the found libraries . Then, perhaps, I will refactor these pieces.

Next, you need to check, suddenly the right RC branches already exist, if there were already attempts to build, then they need to be deleted and new ones created. I already did this using the Python GitLab library:

gl = gitlab.Gitlab('https://gitlab...ru/', private_token=GITLAB_PRIVATE_TOKEN)

pr = gl.projects.get(project)

try:

   rc = pr.branches.get(f'{RC_name}')

   rc.delete()

   pr.branches.create({'branch': f'{RC_name}', 'ref': 'master'})

except gitlab.GitlabError:

   pr.branches.create({'branch': f'{RC_name}', 'ref': 'master'})

After that, you can start filling out the table in the Jira assembly task . The information in the table is contained in the following columns: No., Task, Priority, Merge requests from the task in RC, Status of the merge request (whether the tests passed in Gitlab, whether there are conflicts or not, is poured / not poured).

At this step, I encountered an unpleasant Gitlab flaw: if the changes were previously frozen in the destination branch of the merge request, then the Gitlab API, when requesting the status of the merge request, gives an answer about the presence of conflicts. In principle, this can be understood: if there is nothing to pour in, then it will not work, but why say that there is a conflict in this place? Gitlab forums have been asking people for several years, but there are no answers yet.

In my case, this leads to a false conflict status and manually checking the merge request. I haven’t come up with anything yet.

The logic when creating merge requests is as follows: if tests are not passed, or there is a conflict, merge requests are created, but they do not flow into RC, the corresponding statuses are put in the table.

To check the execution of the tests, we look for a list of suitable pipelines. Gitlab emits them sorted by date in descending order, just as we need. We take the first - it will be the one that is needed:

pipelines = project.pipelines.list(ref=f'{issue}')

if pipelines:

   pipelines = pipelines[0]

   if pipelines.attributes['status'] != 'success':

       status = '(x)   !, '

Then create the merge requests themselves . We check for open ones, in case this is not the first attempt to build. In the absence of the required merge request, create this:

mr = project.mergerequests.list(

 state='opened',

source_branch=source_branch,         target_branch=target_branch)

if mr:

   mr = mr[0]

else:

   mr = project.mergerequests.create(

{'source_branch': source_branch,

           'target_branch': target_branch,

           'title': f"{(MR.issue).replace('-', '_')} -> {RC_name}",

           'target_project_id': PROJECTS_NAMES[MR.project],})

status = mr.attributes['merge_status']

url = mr.attributes['web_url']

return status, url, mr

MR.issue.replace ('-', '_') - change the name of the task to get rid of Gitlaba's uninformative comments on the task in Jira.

Next, we look at the status of the received merge request : if there is no conflict, we pour it into RC. If there is a conflict, put down the appropriate statuses and leave for manual verification.

Further, by analogy, we create merge requests from RC to Staging and from Staging to Master for all projects. They will be poured after checking the entire release build in RC branches.

Two fields were added to the Jira task template: for pre-pre-post and post-de-post actions. For all the tasks involved, the algorithm collects a list of such actions and enters the assembly task in order not to forget anything.

And finally, the output in Jira is the creation of an assembly task :

existing_issue = jira.search_issues(

f'project=PROJ AND summary ~ " {release_name}"')

if existing_issue:

   existing_issue = existing_issue[0]

   existing_issue.update(fields={

       'description': message,

       'customfield_xxx': message_before_deploy,

       'customfield_yyy': message_post_deploy,})

else:

   issue_dict = {

       "fixVersions": [{"name": release_name,}],

       'project': {'key': 'PROJ'},

       'summary': f" {release_name}",

       'description': message,

       'issuetype': {'name': 'RC'},  #      

       'customfield_xxx': message_before_deploy,

       'customfield_yyy': message_post_deploy,}

   new_issue = jira.create_issue(fields=issue_dict)

Further thoughts are as follows : running a script on Gitlab with a web hook from Jira. For example, you can make a webhook to create an assembly task (create a special type of task for it), or create a task with the word “Assembly” in the title. And Gitlab on this hook will either run a bash script that starts the whole process, or raise a Docker image with Python and run a script on it. Although the second option is already too complicated. Yes, and need technical accounts in Jira and Gitlab. In general, there is no final decision yet.

After creating RC branches, you can also deploy a test bench on the necessary branches and run regression tests. Jenkins can handle this. But this is also in the plans for now.

This was all for the sake of accelerating the work of testers and freeing developers from routine work. However, the economic sense here is also quite specific: take an average developer (in a vacuum) with a hypothetical salary of 150,000 rubles for 8 hours of working time per day. For two years, we had about 700 releases - this is about the release per day. Some more, some less, but on average, I think, at least an hour of the developer’s time to build the release was gone. That is, the automation of this process saves the company a minimum of 150,000/8 = 18,750 rubles per month.

Along the way, I made a separate script for myself, showing statistics on the upcoming release: how many tasks, what projects are affected, etc. Since if I do not have Developer status in any of the projects in Gitlab, there will be a denial of access when creating merge requests. Also, it’s convenient to know in advance about deployment actions or to catch a task that fell into this release by mistake. The distribution of release notifications was resolved using the SMTP and email modules.

I was pleased with the solution to this problem: it is always interesting to learn something new and put it into practice. It will be nice if this experience is useful to someone.

All Articles