If you’ve ever had to deal with horrific merge conflicts from infrequently synced branches, I’ve felt your pain. Our team develops off of the Dev branch, promotes code to a QA branch for testing, and merges bug fixes into the QA branch before releasing to production. As we get closer to a release and do more work in QA, Dev can quickly get out of date. And it’s easy to forget to down-merge QA to Dev when you’re focused on getting a release out. But there’s a price to pay: when it comes time to down-merge after the release, resolving the merge conflicts can become a tedious, time-consuming, and error-prone nightmare.
There’s got to be a better way! To get us into the habit of down-merging QA to Dev every day, I wanted to make it as easy and frictionless as possible to keep the branches in sync. So I created a slackbot to open a new GitLab merge request (“MR”) for us and post it to our team Slack channel each morning.
Each weekday morning, the slackbot checks whether QA and Dev are in sync in our three main repos. If so, it notifies us via Slack that the branches are in sync. If not, it opens a new MR from QA to Dev and posts the link to Slack. It lets us know if there are merge conflicts, and if so, it includes resolution instructions.
Avoiding Gnarly Merge Conflicts Through Continuous Integration
Ultimately, the slackbot is a tool to help us maintain good Continuous Integration (“CI”) habits. CI, in its broadest definition, is the practice of frequently integrating code changes into a shared branch, kept in a working state, so that everyone on the team has the most recent code. With respect to branch management, as Martin Fowler puts it, “[t]he over-arching theme is that branches should be integrated frequently and efforts focused on a healthy mainline that can be deployed into production with minimal effort.”
One of the biggest benefits of good CI habits is minimizing Git merge conflicts. Git is a fantastic tool for collaboration, and it can often integrate code changes automatically. But the less frequently branches are integrated and the further the code has diverged, the more likely it is that Git won’t be able to merge the code without human intervention.
Small merge conflicts from recent changes aren’t such a big deal. It’s often clear which side of the conflict is correct, even to another developer on the team that didn’t author the change. But infrequent integration can lead to “nasty” merge conflicts that quickly spiral out of control.
These “nasty” merges can “generate a considerable amount of work”, since the developer resolving the conflicts must first get context on other developers’ changes, then figure out how they should be synchronized, then ensure the code is still in working order. They’re also dangerous, since human error in resolving complex conflicts can introduce hard-to-trace bugs.
CI is a Habit
To avoid these nasty merge conflicts, we need to get into the habit of frequently integrating our branches. Behavioral psychology teaches that habits are easiest to form when they are made easier. If we set out our running shoes ahead of time, there will be less friction in forming the daily habit of running. It’s also easiest to form habits when they are preceded by a consistent cue to perform the behavior, at the same time and place. We can set an alarm on our phone for the same time every day, which will provide a consistent cue to put on our running shoes and hit the road.
The slackbot helps us form good CI habits by making integration as frictionless as possible. It creates a branch and MR for us and sends it right to where we’re already looking. All we need to do is click and review. And integrating daily makes conflicts infrequent and easy to resolve. The slackbot also provides a strong, consistent cue at the same time and place: it sends the message at the same time every work day, right as we’re sitting down to our desks.
Creating the Slackbot with Bash Scripting and the GitLab API
To create the slackbot, I used a Bash script, GitLab, and the GitLab API.
For my initial attempt, I decided to write a Bash shell script so I could use Git’s CLI to diff QA and Dev and skip the GitLab API calls if the branches were already synced. (I used this command:
DIFFCOUNT=$(git diff origin/dev..origin/qa | wc -m | tr -d " ")). That ended up being more trouble than it was worth, both because it’s easier to manage multiple repos using only GitLab’s API, and because GitLab’s diffing algorithm is different than the diff command above, leading to unmergeable empty MRs. (See these issues.)
While I kept the script in Bash after refactoring away the use of the Git CLI, the script could easily be converted to Node or another language.
If you’re relatively new to Bash scripting like me, I think you’ll find that it becomes more straightforward once you get past the terse, outdated syntax and get some experience. But it does require a somewhat different way of thinking about how to solve problems than you might be used to.
Using some modern tooling in VS Code helps. I’d recommend installing the Bash IDE for syntax highlighting and code completion, Shellcheck for code formatting and linting, and Shellman for code snippets.
Explainshell is also helpful for breaking down dense and confusing lines of Bash code. Try pasting in some of the lines of code in this blog for more detailed explanations.
The Frontend Masters course Complete Intro to Linux and the Command-Line is an excellent place to start learning more. And if you’d like more depth, Learning the Bash Shell is a very thorough primer.
Once you’ve got your tooling set up, follow these steps.
1. Gather Credentials
To use the Gitlab API, you’ll need to generate a personal access token. Follow the steps here, and make sure to choose the
api scope. Save the token somewhere safe.
Gitlab Project ID(s)
For each repo in which you’d like to use the slackbot, navigate to the project homepage and copy down the “Project ID”, located directly under the project name.
You’ll need to create a slack app by clicking “create app” at https://api.slack.com/apps. On your app’s homepage, click “add features and functionality”, then “incoming webhooks”, then click activate.
Create two webhooks: one that sends you a DM for testing (search for your name), and the “production” webhook that posts to your team’s Slack channel. Keep the webhook URLs secret and safe.
On your app’s home page, you can add a name, image, and description that will be shown when the slackbot posts messages.
2. Setup a Slackbot Branch in one of your Repos
In one of the repos you want to manage with the slackbot, checkout a new branch (e.g.,
js/slackbot). You can have the slackbot create MRs for multiple repos from this single branch.
Create a .env file
.env file (make sure it’s git-ignored!) and paste in your GitLab variables and Slack webhook url for testing locally. (The variables will be set in the GitLab console for the deployed job). Make sure they are exported so they are available to subshells.
Because Bash lacks complex data structures, we’ll use string matching to map each repo’s display name to its GitLab ID in our environment variables. First, create a comma-separated string listing the single-word display names of the repos you will manage with the slackbot. For example,
export REPO_DISPLAY_NAMES="Frontend, Services, Personalization". For each repo in that list, create a matching ID variable with the repo name in all caps followed by _ID, such as
PERSONALIZATION_ID. The display name variable in the list and the ID variable name before the
_ID must match exactly (aside from case). Here’s an example:
The benefit of this approach is that adding another repo for the slackbot to manage is as easy as adding another repo name to the list and another repo ID variable.
Create the script file
Create a file called
slackbot.sh in your repo, perhaps in a
ci folder. Open it and add the Bash shebang on the first line:
Make the script executable. On the command line, run
chmod u+x slackbot.sh
Edit your gitlab-ci.yml
Since the slackbot doesn’t really fit into the traditional build, test, and deploy stages, add a slackbot stage to the stages key.
And add a slackbot job that will execute the script.
You’ll want to ensure that only the slackbot gets triggered when the scheduled job runs. For safety, I keep the slackbot in its own branch, the scheduled job targets that branch, and all the other jobs explicitly do not run on schedules. For each other job, you could add:
3. Create the Slackbot Bash Script
Our slackbot will use another dedicated branch for creating the MRs from QA to Dev —
slackbot/qa-to-dev. This will help later with merge conflicts.
Generally, our script will:
- Read in the array of repos to be managed and call the handle_repo function for each repo.
- For each repo, start with a clean slate by deleting the
slackbot/qa-to-devbranch (if it exists) and creating a fresh
slackbot/qa-to-devbranch off of QA.
- Create an MR from that branch to Dev.
- See if merge conflicts exists.
- Send the link to the MR and any merge conflict info to Slack.
A. Load environment variables.
First, add the following code to load your
.env when testing the script locally.
We’ll exit with an error code if the necessary variables aren’t present.
B. Call the handle_repo function for each repo.
First, transform the comma-separated list of repo names in
REPO_DISPLAY_NAMES environment variable into an array:
Then, for each repo, we’ll call our
handle_repo function by looping over the array of repo display names, capitalizing the name and adding
_ID to get the name of that repo’s ID variable, then passing in the repo display name and the repo ID as function arguments.
C. Setup the function and variables.
handle_repo function and setup the variables you’ll need. Note that the slackbot branch name must be url-escaped for the GitLab API calls:
D. Start with a clean slate.
So that we can follow the same steps every time, even if we forgot to merge in yesterday’s MR, we’ll delete the current
slackbot/qa-to-dev branch if it exists. This (usually) will delete the old MR too. Then we’ll create a new
slackbot/qa-to-dev branch off of QA.
E. Create the MR and get MR info.
We’ll create a new MR from
slackbot/qa-to-dev to Dev using the GitLab API, and get the MR ID from the response.
Then, we’ll get the MR’s info from the GitLab API. I’ve found we need to
sleep for 10 seconds before fetching the MR info to let GitLab finish calculating merge conflict info.
We’ll use Python to parse the JSON response and extract the MR’s URL, then we’ll add the URL to our Slack message.
F. Report on merge conflicts.
The MR info API response tells us whether there are merge conflicts. If so, we’ll append instructions for resolving them to our Slack message. Since we’re using a separate branch, anyone on the team can just pull down
slackbot/qa-to-dev, merge Dev into it locally, resolve the conflicts, then push back up. That way, no one has to create a new branch.
Note that we can use Slack text formatting and emojis in our message. 🎉
Also note that when using Python to parse JSON,
true values become uppercase
null values become
G. Close empty MRs.
GitLab will still create an “empty” MR even if the branches are in sync, so we’ll need to close the MR if it comes back without any changes. If so, we’ll set our
MSG variable to let the team know the branches are already synced for that repo.
H. Send to Slack!
Finally, we’re ready to send our message to Slack. We’ll simply send JSON to our webhook, with a key of
text pointing to our
MSG variable that we set above, escaping the interior quotation marks.
I. Test Locally
That’s the script! We can try running it locally to send a test DM to ourselves and make sure everything works.
4. Setup the Scheduled Job
Almost there! All that’s left is to configure the scheduled job on GitLab.
On the GitLab menu in your project, navigate to CI/CD -> Schedules, and click “Create New Schedule”.
Set the cron signature and timezone. I set the job to run every weekday at 9am Eastern with this cron signature:
0 9 * * 1-5. (Checkout crontab.guru to test out cron signatures.)
Set the target branch to the branch in which you wrote the slackbot script. (E.g.,
js/slackbot, not the
$SLACKBOT_BRANCH that the slackbot will use to create MRs!)
Enter in your environment variables, set the job to activated, and click save.
I’d recommend testing it out first by setting the
SLACK_URL variable to send you a DM, and clicking the play button on the job. If all goes well, switch the
SLACK_URL to send the message to your team’s channel.
And TADA! You’ve got yourself a slackbot.
The current version of the script assumes that each repo has the same source/target branch names (qa and dev), but that could be made configurable for different branch names or for syncing multiple branches within a repo. And if the script’s complexity grows much further in future editions, it’s probably time to move it over to Node.
And that’s how you set up your very own slackbot! It’ll help you and your team establish good branch integration habits and avoid frustrating, time-consuming, and dangerous merge conflicts. It’s also a fun way to get experience with Bash scripting, GitLab, and Slack Apps. These building blocks open up lots of possibilities for automating away pain-points and improving your team’s development experience.
The coolest thing about learning skills like these is that you’re no longer limited to the productivity tools handed down to you. You’re empowered to create tools that fit your team’s unique needs.
Full Slackbot Script
Also available at this gist.