<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>Forem: Josh Stillman</title>
    <description>The latest articles on Forem by Josh Stillman (@joshstillman).</description>
    <link>https://forem.com/joshstillman</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F405451%2Fe062faa4-18a9-4a53-b9b3-28370762d0aa.png</url>
      <title>Forem: Josh Stillman</title>
      <link>https://forem.com/joshstillman</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://forem.com/feed/joshstillman"/>
    <language>en</language>
    <item>
      <title>Embrace Your Laziness: Automatically Convert Word Documents into Terms &amp; Conditions Pages</title>
      <dc:creator>Josh Stillman</dc:creator>
      <pubDate>Fri, 07 Jan 2022 15:02:30 +0000</pubDate>
      <link>https://forem.com/joshstillman/embrace-your-laziness-automatically-convert-word-documents-into-terms-conditions-pages-5gb</link>
      <guid>https://forem.com/joshstillman/embrace-your-laziness-automatically-convert-word-documents-into-terms-conditions-pages-5gb</guid>
      <description>&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fp9t9nzu0hoiua92q6tvt.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fp9t9nzu0hoiua92q6tvt.gif" alt="textutil in action"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Modern Single Page Applications (SPAs) often &lt;a href="https://www.mindtheproduct.com/improving-ux-terms-conditions-page-6-easy-ways/" rel="noopener noreferrer"&gt;embed terms and conditions pages&lt;/a&gt; into the app itself for a slick and modern feel. While this makes for a great user experience, it can be tedious and time consuming for developers to convert long Microsoft Word documents of legal copy into HTML/JSX that can be embedded into a terms and conditions component or modal. But fret not, fellow developer! With some macOS and shell utilities, you can let the computer handle the drudgery, so you can focus on something more important.&lt;/p&gt;

&lt;p&gt;Simply using some built-in command line programs in macOS will do the trick, converting the Word document into clean HTML you can paste into your terms and conditions component! Let's see how—and why.&lt;/p&gt;

&lt;h1&gt;
  
  
  Laziness Is a Virtue
&lt;/h1&gt;

&lt;p&gt;Larry Wall, the creator of the Perl programming language, argued that laziness is one of the &lt;a href="http://threevirtues.com/" rel="noopener noreferrer"&gt;primary virtues&lt;/a&gt; of a good programmer. It's what makes coders "write labor-saving programs that other people will find useful." And indeed, a good engineer will "go to great effort to reduce overall energy expenditure" by finding opportunities to make processes more efficient.&lt;/p&gt;

&lt;p&gt;Along these lines, I'd say that a primary coding virtue is the ability to identify which tasks are rote, repetitive, and best delegated to a computer, and which tasks instead require human creativity, problem solving, and ingenuity. We'll only ever have time and energy for the latter category if we find a way to let the computers handle the boring, repetitive stuff.&lt;/p&gt;

&lt;h1&gt;
  
  
  The Inevitable Terms &amp;amp; Conditions Ticket
&lt;/h1&gt;

&lt;p&gt;It's inevitable. When developing a new SPA, there will come a day that you or your teammate will be assigned a ticket to create an embedded terms and conditions page or modal. (That's our litigious modern society. Sigh...) Typically, a developer is handed a Word document from the legal department and some fancy designs, and left to figure out the rest.&lt;/p&gt;

&lt;p&gt;The most painstaking approach would be to manually copy each paragraph, add any bold and italic formatting, and wrap it in appropriate HTML tags. This can take a while if it's a long Word document! And it won't be pleasant. Our laziness instincts should be kicking in about now.&lt;/p&gt;

&lt;p&gt;We can make this process a little less manual through this nifty &lt;a href="https://marketplace.visualstudio.com/items?itemName=bradgashler.htmltagwrap" rel="noopener noreferrer"&gt;VS Code extension&lt;/a&gt;. It will let us wrap each paragraph or sentence of text in the appropriate &lt;code&gt;&amp;lt;p&amp;gt;&lt;/code&gt;, &lt;code&gt;&amp;lt;b&amp;gt;&lt;/code&gt;/&lt;code&gt;&amp;lt;strong&amp;gt;&lt;/code&gt;, or &lt;code&gt;&amp;lt;i&amp;gt;&lt;/code&gt;/&lt;code&gt;&amp;lt;em&amp;gt;&lt;/code&gt; tags. But it's still a pretty manual process of copying, pasting, and formatting. How can we fully automate this?&lt;/p&gt;

&lt;h1&gt;
  
  
  There's a CLI for That
&lt;/h1&gt;

&lt;p&gt;Good news! macOS ships with a command line tool called &lt;code&gt;textutil&lt;/code&gt; that excels at converting documents into different formats. It can convert a Word document into HTML in a single terminal command: &lt;code&gt;textutil -convert html -strip terms.docx&lt;/code&gt;. This will take your Word document, strip out all the metadata, and convert it into basic HTML markup. Paragraphs will be wrapped in &lt;code&gt;&amp;lt;p&amp;gt;&lt;/code&gt; tags, and bold and italic formatting tags will be added as well. No more need to go through the document paragraph by paragraph yourself to create the markup. And it even works on other document formats, such as .txt and .rtf files. Joy!&lt;/p&gt;

&lt;h1&gt;
  
  
  Much Too Classy
&lt;/h1&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fgfvd6bxknzaiwstb8pxt.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fgfvd6bxknzaiwstb8pxt.png" alt="Initial Output"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;One problem! &lt;code&gt;textutil&lt;/code&gt; creates some basic CSS styles for you based on the source Word document and attaches very generic class names such as &lt;code&gt;p2&lt;/code&gt; and &lt;code&gt;Apple-converted-space&lt;/code&gt; to seemingly every tag it creates. But you probably don't want these generated class names polluting your markup. Not only does it just look ugly and hard to read, but these highly generic class names could clash with other classes in your app, leading to unintended consequences.&lt;/p&gt;

&lt;p&gt;Sadly, &lt;code&gt;textutil&lt;/code&gt; lacks any built-in option to suppress these class names. Sure, we could manually remove all the classes from the generated markup, but we don't want to do that either.&lt;/p&gt;

&lt;h1&gt;
  
  
  Right Sed Fred
&lt;/h1&gt;

&lt;p&gt;Fear not—we can clean up the HTML that &lt;code&gt;textutil&lt;/code&gt; gives us using &lt;code&gt;sed&lt;/code&gt;, a shell tool for text manipulation that comes built into Bash and Zsh. We'll pipe the HTML that &lt;code&gt;textutil&lt;/code&gt; generates into &lt;code&gt;sed&lt;/code&gt;, strip out all the class names, and save the result to a file.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;sed&lt;/code&gt; command we'll use to delete the class names is &lt;code&gt;sed 's/class="[^"]*"//g'&lt;/code&gt;. Let's break that down. The leading &lt;code&gt;s&lt;/code&gt; in the argument means we'll substitute text matching the pattern between the first and second &lt;code&gt;/&lt;/code&gt; characters with the text between the second and third &lt;code&gt;/&lt;/code&gt;'s. The regex pattern we'll match is &lt;code&gt;class="[^"]*"&lt;/code&gt; (explained below). Then, we'll replace the text matching that pattern with the text between the last two slashes—here, an empty string. And we'll do it for every occurrence with the global modifier, &lt;code&gt;/g&lt;/code&gt;. That is, we'll simply delete the text matching the pattern throughout the document.&lt;/p&gt;

&lt;p&gt;About that funky-looking regex... &lt;code&gt;sed&lt;/code&gt; doesn't have the same regex capabilities you're familiar with in modern languages such as JavaScript. It doesn't have &lt;a href="https://javascript.info/regexp-greedy-and-lazy" rel="noopener noreferrer"&gt;lazy matching&lt;/a&gt;, meaning that if you try to match &lt;code&gt;class=".*"&lt;/code&gt;, &lt;code&gt;sed&lt;/code&gt; will greedily match far more text than you intended, well beyond the end of the HTML tag.&lt;/p&gt;

&lt;p&gt;Instead, we can mock lazy matching in &lt;code&gt;sed&lt;/code&gt; with &lt;a href="https://unix.stackexchange.com/questions/297686/non-greedy-match-with-sed-regex-emulate-perls/397813#397813" rel="noopener noreferrer"&gt;this&lt;/a&gt; &lt;a href="https://0x2a.at/blog/2008/07/sed--non-greedy-matching/" rel="noopener noreferrer"&gt;technique&lt;/a&gt;: we can match the opening &lt;code&gt;"&lt;/code&gt;, followed by any character &lt;em&gt;except&lt;/em&gt; a &lt;code&gt;"&lt;/code&gt;, then the closing &lt;code&gt;"&lt;/code&gt;. So &lt;code&gt;/class="[^"]*"/&lt;/code&gt; will get us the lazy matching we need—effectively &lt;code&gt;/class=".*?"/&lt;/code&gt; in JavaScript's regex dialect. Lazy matching for lazy programmers!&lt;/p&gt;

&lt;p&gt;After running &lt;code&gt;textutil&lt;/code&gt;'s output through this &lt;code&gt;sed&lt;/code&gt; command, we'll have nice, clean markup without all the random class names.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fjl0whghryrhihiya6w5j.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fjl0whghryrhihiya6w5j.png" alt="Transformed output"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Building on this technique, we could even take it a step further and strip out unnecessary &lt;code&gt;&amp;lt;span&amp;gt;&lt;/code&gt; tags, and anything else we wanted to get rid of from &lt;code&gt;textutil&lt;/code&gt;'s output.&lt;/p&gt;

&lt;h1&gt;
  
  
  Putting It All Together
&lt;/h1&gt;

&lt;p&gt;Last, we'll save the cleaned HTML to a file. The final command line script is &lt;code&gt;textutil -convert html -strip -stdout terms.docx | sed 's/ class="[^"]*"//g' &amp;gt; output.html&lt;/code&gt;, which (1) converts the Word document to HTML with &lt;code&gt;textutil&lt;/code&gt;, (2) strips out the class names that &lt;code&gt;textutil&lt;/code&gt; adds to each tag with &lt;code&gt;sed&lt;/code&gt;, and (3) saves the cleaned HTML to a file. From there, we can simply paste the HTML into our terms and conditions component in our SPA, style it, and call it a day.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fp9t9nzu0hoiua92q6tvt.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fp9t9nzu0hoiua92q6tvt.gif" alt="textutil in action"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h1&gt;
  
  
  Conclusion
&lt;/h1&gt;

&lt;p&gt;If a development task is manual, repetitive, time-consuming, and boring, &lt;a href="https://kentcdodds.com/blog/automation" rel="noopener noreferrer"&gt;that's a sign&lt;/a&gt;. As developers, we should hone a keen awareness of this feeling, which is usually a clear sign that it's time to automate the task and move on to more creative, higher-value problem solving.  It's a unique privilege of being software engineers that we can (and should!) automate these annoying parts of our jobs. So, embrace your laziness, fellow devs! It's the virtuous thing to do.&lt;/p&gt;

&lt;h1&gt;
  
  
  TL;DR
&lt;/h1&gt;

&lt;p&gt;Convert your Word document to clean HTML on macOS by running this command in your shell: &lt;code&gt;textutil -convert html -strip -stdout terms.docx | sed 's/ class="[^"]*"//g' &amp;gt; output.html&lt;/code&gt;&lt;/p&gt;

</description>
      <category>productivity</category>
      <category>webdev</category>
      <category>automation</category>
      <category>html</category>
    </item>
    <item>
      <title>Instantly Create Gmail Addresses for Testing with a Keyboard Shortcut on Mac OS</title>
      <dc:creator>Josh Stillman</dc:creator>
      <pubDate>Mon, 08 Mar 2021 15:00:34 +0000</pubDate>
      <link>https://forem.com/joshstillman/instantly-create-gmail-addresses-for-testing-with-a-keyboard-shortcut-on-mac-os-2jc1</link>
      <guid>https://forem.com/joshstillman/instantly-create-gmail-addresses-for-testing-with-a-keyboard-shortcut-on-mac-os-2jc1</guid>
      <description>&lt;p&gt;&lt;a href="https://media.dev.to/cdn-cgi/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fn5lcy32ttramows9i1fl.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/cdn-cgi/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fn5lcy32ttramows9i1fl.gif" alt="The Shortcut in Action" width="487" height="247"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;When developing new features, it can be useful to be able to create new, unique, working email addresses for testing. For example, say you want to repeatedly test a sign-up flow end-to-end, and you need a new, unique email address each time through the flow. Furthermore, you'd like to use a real, working email address to verify the behavior of a confirmation email that gets sent after signing up. How can we easily accomplish this?&lt;/p&gt;

&lt;p&gt;If you have a Gmail or &lt;a href="https://workspace.google.com/"&gt;Google Workspace&lt;/a&gt; (formerly G Suite) email address, you're in luck! One of Gmail's coolest under-the-radar features is the ability to &lt;a href="https://support.google.com/a/users/answer/9308648?hl=en"&gt;create task-specific email addresses&lt;/a&gt; by adding a plus sign followed by additional characters to your email address. So, if your email address is &lt;code&gt;bob.smith@gmail.com&lt;/code&gt;, you can sign up with &lt;code&gt;bob.smith+hello-world@gmail.com&lt;/code&gt;, and emails to &lt;code&gt;bob.smith+hello-world@gmail.com&lt;/code&gt; will reach your inbox. This makes it easy to repeatedly create new, unique, working addresses for testing!&lt;/p&gt;

&lt;p&gt;But if you do this a lot, it becomes mentally draining to keep track of which email addresses you've already used and come up with new addresses on the fly. You'll have to keep track of whether you've already used &lt;code&gt;bob.smith+test15@gmail.com&lt;/code&gt; or should instead be on &lt;code&gt;bob.smith+test16@gmail.com&lt;/code&gt;, for example. Instead, let's automate it!&lt;/p&gt;

&lt;p&gt;With some built-in Mac OS tools, it's possible to automatically create new, unique Gmail addresses with a handy keyboard shortcut to speed up our smoke testing. First, we'll use the Mac OS Automator application to create a script to generate new email addresses using the current system time. Then we'll attach that script to a keyboard shortcut in our system keyboard preferences.&lt;/p&gt;

&lt;h1&gt;
  
  
  Automator Quick Action
&lt;/h1&gt;

&lt;p&gt;Mac OS ships with the &lt;a href="https://support.apple.com/guide/automator/welcome/mac"&gt;Automator&lt;/a&gt; program that allows you to create scripts using JavaScript or your shell, and these scripts can output text. In Automator, select &lt;em&gt;New&lt;/em&gt;, then choose &lt;em&gt;Quick Action&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/cdn-cgi/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fq60ngoip4zk51s8zm0ox.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/cdn-cgi/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fq60ngoip4zk51s8zm0ox.png" alt="Choose _Quick Action_" width="571" height="499"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;On the left hand pane, select &lt;em&gt;Utilities&lt;/em&gt;, then &lt;em&gt;Run JavaScript&lt;/em&gt; or &lt;em&gt;Run Shell Script&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/cdn-cgi/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fgwbid0w3o6eytk8v102v.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/cdn-cgi/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fgwbid0w3o6eytk8v102v.png" alt="Choose _Utilities_ then _Run JavaScript_" width="444" height="626"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;On the top pane, select &lt;em&gt;Workflow receives no input in any application&lt;/em&gt;. Also select the &lt;em&gt;Output replaces selected text&lt;/em&gt; checkbox.&lt;/p&gt;

&lt;p&gt;In the &lt;em&gt;Run JavaScript&lt;/em&gt; dialog below, create a script that will interpolate the &lt;a href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/getTime"&gt;system time in milliseconds&lt;/a&gt; into your Gmail address after the + sign. Using ES6 template literals, you can have the function:&lt;br&gt;
&lt;br&gt;
 &lt;code&gt;return `bob.smith+${new Date().getTime()}@gmail.com`;&lt;/code&gt;&lt;br&gt;
&lt;br&gt;
 This will return an email in the format &lt;code&gt;bob.smith+1613507754883@gmail.com&lt;/code&gt;. Make sure not to check &lt;em&gt;Show this action when the workflow runs&lt;/em&gt;, otherwise Automator will show a pop-up each time. Then, save and name your Automator action.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/cdn-cgi/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fu7dptdl3dh9ovev2pjae.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/cdn-cgi/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fu7dptdl3dh9ovev2pjae.png" alt="Write your JavaScript" width="481" height="366"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;You can also use Automator to execute shell scripts, which opens up all kinds of possibilities.  The equivalent shell script here (in seconds) would be &lt;code&gt;printf "bob.smith+%s@gmail.com" $(date +%s)&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/cdn-cgi/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fokgi5sjan2umg42tc9ew.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/cdn-cgi/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fokgi5sjan2umg42tc9ew.png" alt="Or write a shell script" width="487" height="307"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h1&gt;
  
  
  Keyboard Shortcut
&lt;/h1&gt;

&lt;p&gt;Next, open &lt;em&gt;System Preferences&lt;/em&gt;, choose &lt;em&gt;Keyboard&lt;/em&gt;, then &lt;em&gt;Shortcuts&lt;/em&gt;. In the left-hand pane, choose &lt;em&gt;Services&lt;/em&gt;. You should see the Quick Action you created with Automator as an option in the right-hand pane. Add a memorable keyboard shortcut, then click the checkbox to activate. (I chose Control + Option + Command + C, but you might want to investigate setting up a &lt;a href="https://syntax.fm/show/315/hasty-treat-hyper-productivity-with-keyboard-shortcuts-window-management"&gt;Hyper Key&lt;/a&gt; for things like this instead.)&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/cdn-cgi/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fmggutoirr2wh1961t92h.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/cdn-cgi/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fmggutoirr2wh1961t92h.png" alt="Set the Keyboard Shorcut under _Services_" width="645" height="426"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Now, give your keyboard shortcut a try!&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/cdn-cgi/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fn5lcy32ttramows9i1fl.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/cdn-cgi/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fn5lcy32ttramows9i1fl.gif" alt="The Shortcut in Action" width="487" height="247"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This is one simple example of how to use scripting, Mac OS Automator, and keyboard shortcut mappings to automate simple tasks and speed up your development experience. But these tools open up tons of other possibilities. Got any cool ideas of your own? Let us know in the comments!&lt;/p&gt;

</description>
      <category>productivity</category>
      <category>testing</category>
      <category>javascript</category>
      <category>shell</category>
    </item>
    <item>
      <title>Slackbot to the Rescue: Keeping GitLab Branches in Sync with a Slackbot</title>
      <dc:creator>Josh Stillman</dc:creator>
      <pubDate>Thu, 28 Jan 2021 16:12:25 +0000</pubDate>
      <link>https://forem.com/joshstillman/slackbot-to-the-rescue-keeping-gitlab-branches-in-sync-with-a-slackbot-jl8</link>
      <guid>https://forem.com/joshstillman/slackbot-to-the-rescue-keeping-gitlab-branches-in-sync-with-a-slackbot-jl8</guid>
      <description>&lt;p&gt;&lt;a href="https://media.dev.to/cdn-cgi/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fi%2Fpsv83e0ls2rn19885wsn.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/cdn-cgi/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fi%2Fpsv83e0ls2rn19885wsn.png" alt="Alt Text" width="690" height="228"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;h1&gt;
  
  
  Avoiding Gnarly Merge Conflicts Through Continuous Integration
&lt;/h1&gt;

&lt;p&gt;Ultimately, the slackbot is a tool to help us maintain good Continuous Integration ("CI") habits. CI, in its &lt;a href="https://aws.amazon.com/devops/continuous-integration/"&gt;broadest definition&lt;/a&gt;, 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 &lt;a href="https://martinfowler.com/articles/branching-patterns.html"&gt;puts it&lt;/a&gt;, "[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."&lt;/p&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;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 &lt;a href="https://martinfowler.com/articles/branching-patterns.html"&gt;"nasty"&lt;/a&gt; merge conflicts that quickly spiral out of control.&lt;/p&gt;

&lt;p&gt;These "nasty" merges can &lt;a href="https://martinfowler.com/articles/branching-patterns.html"&gt;"generate a considerable amount of work"&lt;/a&gt;, 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.&lt;/p&gt;

&lt;h2&gt;
  
  
  CI is a Habit
&lt;/h2&gt;

&lt;p&gt;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 &lt;a href="https://www.psychologytoday.com/us/blog/brain-wise/201904/the-science-habits"&gt;made easier&lt;/a&gt;. If we set out our running shoes ahead of time, there will be less friction in forming the habit of running.  It's also easiest to form habits when they are preceded by a &lt;a href="https://charlesduhigg.com/how-habits-work/"&gt;consistent cue&lt;/a&gt; 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.&lt;/p&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;h1&gt;
  
  
  Creating the Slackbot with Bash Scripting and the GitLab API
&lt;/h1&gt;

&lt;p&gt;To create the slackbot, I used a Bash script, GitLab, and the GitLab API.&lt;/p&gt;

&lt;p&gt;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 are already synced.  (I used this command: &lt;code&gt;DIFFCOUNT=$(git diff origin/dev..origin/qa | wc -m | tr -d " ")&lt;/code&gt;).  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 &lt;a href="https://gitlab.com/gitlab-org/gitlab/-/issues/27008"&gt;these&lt;/a&gt; &lt;a href="https://gitlab.com/gitlab-org/gitlab-foss/-/issues/15140"&gt;issues&lt;/a&gt;.)&lt;/p&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;h2&gt;
  
  
  Bash Tooling
&lt;/h2&gt;

&lt;p&gt;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 &lt;a href="https://en.wikipedia.org/wiki/Unix_philosophy#Origin"&gt;different way of thinking&lt;/a&gt; about how to solve problems than you might be used to.&lt;/p&gt;

&lt;p&gt;Using some modern tooling in VS Code helps. I'd recommend installing the &lt;a href="https://marketplace.visualstudio.com/items?itemName=mads-hartmann.bash-ide-vscode"&gt;Bash IDE&lt;/a&gt; for syntax highlighting and code completion, &lt;a href="https://marketplace.visualstudio.com/items?itemName=timonwong.shellcheck"&gt;Shellcheck&lt;/a&gt; for code formatting and linting, and &lt;a href="https://marketplace.visualstudio.com/items?itemName=Remisa.shellman"&gt;Shellman&lt;/a&gt; for code snippets.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://explainshell.com/"&gt;Explainshell&lt;/a&gt; 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.&lt;/p&gt;

&lt;p&gt;The Frontend Masters course &lt;a href="https://frontendmasters.com/courses/linux-command-line/"&gt;Complete Intro to Linux and the Command-Line&lt;/a&gt; is an excellent place to start learning more. And if you'd like more depth, &lt;a href="https://www.oreilly.com/library/view/learning-the-bash/0596009658/"&gt;Learning the Bash Shell&lt;/a&gt; is a very thorough primer.&lt;/p&gt;

&lt;p&gt;Once you've got your tooling set up, follow these steps.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. Gather Credentials
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Gitlab Token
&lt;/h3&gt;

&lt;p&gt;To use the Gitlab API, you'll need to generate a personal access token. Follow the steps &lt;a href="https://docs.gitlab.com/ee/user/profile/personal_access_tokens.html#creating-a-personal-access-token"&gt;here&lt;/a&gt;, and make sure to choose the &lt;code&gt;api&lt;/code&gt; scope. Save the token somewhere safe.&lt;/p&gt;

&lt;h3&gt;
  
  
  Gitlab Project ID(s)
&lt;/h3&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;h3&gt;
  
  
  Slack App
&lt;/h3&gt;

&lt;p&gt;You'll need to create a slack app by clicking "create app" at &lt;a href="https://api.slack.com/apps"&gt;https://api.slack.com/apps&lt;/a&gt;. On your app's homepage, click "add features and functionality", then "incoming webhooks", then click activate.&lt;/p&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;On your app's home page, you can add a name, image, and description that will be shown when the slackbot posts messages.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Setup a Slackbot Branch in one of your Repos
&lt;/h2&gt;

&lt;p&gt;In one of the repos you want to manage with the slackbot, checkout a new branch (e.g., &lt;code&gt;js/slackbot&lt;/code&gt;). You can have the slackbot create MRs for multiple repos from this single branch.&lt;/p&gt;

&lt;h3&gt;
  
  
  Create a .env file
&lt;/h3&gt;

&lt;p&gt;Create a &lt;code&gt;.env&lt;/code&gt; 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.&lt;/p&gt;

&lt;p&gt;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, &lt;code&gt;export REPO_DISPLAY_NAMES="Frontend, Services, Personalization"&lt;/code&gt;. For each repo in that list, create a matching ID variable with the repo name in all caps followed by _ID, such as &lt;code&gt;FRONTEND_ID&lt;/code&gt;, &lt;code&gt;SERVICES_ID&lt;/code&gt;, and &lt;code&gt;PERSONALIZATION_ID&lt;/code&gt;. The display name variable in the list and the ID variable name before the &lt;code&gt;_ID&lt;/code&gt; must match exactly (aside from case).  Here's an example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;export REPO_DISPLAY_NAMES="Frontend, Services, Personalization"

export FRONTEND_ID=
export SERVICES_ID=
export PERSONALIZATION_ID=

export GITLAB_TOKEN=
export SLACK_URL=
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;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.&lt;/p&gt;

&lt;h3&gt;
  
  
  Create the script file
&lt;/h3&gt;

&lt;p&gt;Create a file called &lt;code&gt;slackbot.sh&lt;/code&gt; in your repo, perhaps in a &lt;code&gt;ci&lt;/code&gt; folder. Open it and add the Bash shebang on the first line: &lt;code&gt;#!/usr/bin/env bash&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://www.andrewcbancroft.com/blog/musings/make-bash-script-executable/"&gt;Make the script executable&lt;/a&gt;. On the command line, run &lt;code&gt;chmod u+x slackbot.sh&lt;/code&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Edit your gitlab-ci.yml
&lt;/h3&gt;

&lt;p&gt;Since the slackbot doesn't really fit into the traditional build, test, and deploy stages, add a slackbot stage to the stages key.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;stages&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;slackbot&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And add a slackbot job that will execute the script.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;slackbot&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;stage&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;slackbot&lt;/span&gt;
  &lt;span class="na"&gt;only&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;schedules&lt;/span&gt;
  &lt;span class="na"&gt;script&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;ci/slackbot.sh&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;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:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;  &lt;span class="na"&gt;except&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;schedules&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  3. Create the Slackbot Bash Script
&lt;/h2&gt;

&lt;p&gt;Our slackbot will use another dedicated branch for creating the MRs from QA to Dev -- &lt;code&gt;slackbot/qa-to-dev&lt;/code&gt;.  This will help later with merge conflicts.&lt;/p&gt;

&lt;p&gt;Generally, our script will:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Read in the array of repos to be managed and call the handle_repo function for each repo.&lt;/li&gt;
&lt;li&gt;For each repo, start with a clean slate by deleting the &lt;code&gt;slackbot/qa-to-dev&lt;/code&gt; branch (if it exists) and creating a fresh &lt;code&gt;slackbot/qa-to-dev&lt;/code&gt; branch off of QA.&lt;/li&gt;
&lt;li&gt;Create an MR from that branch to Dev.&lt;/li&gt;
&lt;li&gt;See if merge conflicts exists.&lt;/li&gt;
&lt;li&gt;Send the link to the MR and any merge conflict info to Slack.&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  A. Load environment variables.
&lt;/h3&gt;

&lt;p&gt;First, add the following code to load your &lt;code&gt;.env&lt;/code&gt; when testing the script locally.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="nt"&gt;-f&lt;/span&gt; &lt;span class="s2"&gt;"./.env"&lt;/span&gt; &lt;span class="o"&gt;]&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then
    &lt;/span&gt;&lt;span class="nb"&gt;source&lt;/span&gt; ./.env
    &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"Importing env vars from .env file"&lt;/span&gt;
&lt;span class="k"&gt;fi&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We'll exit with an error code if the necessary variables aren't present.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="nt"&gt;-z&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$SLACK_URL&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;]&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="nt"&gt;-z&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$GITLAB_TOKEN&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;]&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="nt"&gt;-z&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$REPO_DISPLAY_NAMES&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;]&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;  &lt;span class="k"&gt;then
  &lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"Missing environment variables: SLACK_URL: &lt;/span&gt;&lt;span class="nv"&gt;$SLACK_URL&lt;/span&gt;&lt;span class="s2"&gt;, GITLAB_TOKEN: &lt;/span&gt;&lt;span class="nv"&gt;$GITLAB_TOKEN&lt;/span&gt;&lt;span class="s2"&gt;, REPO_DISPLAY_NAMES: &lt;/span&gt;&lt;span class="nv"&gt;$REPO_DISPLAY_NAMES&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
  &lt;span class="nb"&gt;exit &lt;/span&gt;1
&lt;span class="k"&gt;fi&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  B. Call the handle_repo function for each repo.
&lt;/h3&gt;

&lt;p&gt;First, transform the comma-separated list of repo names in &lt;code&gt;REPO_DISPLAY_NAMES&lt;/code&gt; environment variable into an &lt;a href="https://stackoverflow.com/questions/10586153/split-string-into-an-array-in-bash"&gt;array&lt;/a&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;IFS&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;', '&lt;/span&gt; &lt;span class="nb"&gt;read&lt;/span&gt; &lt;span class="nt"&gt;-r&lt;/span&gt; &lt;span class="nt"&gt;-a&lt;/span&gt; REPO_ARRAY &lt;span class="o"&gt;&amp;lt;&amp;lt;&amp;lt;&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$REPO_DISPLAY_NAMES&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then, for each repo, we'll call our &lt;code&gt;handle_repo&lt;/code&gt; function by looping over the array of repo display names, capitalizing the name and adding &lt;code&gt;_ID&lt;/code&gt; to get the name of that repo's ID variable, then passing in the repo display name and the repo ID as function arguments.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="k"&gt;for &lt;/span&gt;REPO &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;REPO_ARRAY&lt;/span&gt;&lt;span class="p"&gt;[@]&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;do
  &lt;/span&gt;&lt;span class="nv"&gt;REPO_ID&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$REPO&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; | &lt;span class="nb"&gt;tr&lt;/span&gt; &lt;span class="s1"&gt;'[:lower:]'&lt;/span&gt; &lt;span class="s1"&gt;'[:upper:]'&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;_ID"&lt;/span&gt; &lt;span class="c"&gt;# Capitalize repo display name and add "_ID" to get project id's variable name&lt;/span&gt;

  handle_repo &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$REPO&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="p"&gt;!REPO_ID&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="c"&gt;# Indirect variable. REPO_ID's value is another variable's name, such as FRONTEND_ID, and we access that second variable's value.&lt;/span&gt;
  &lt;span class="c"&gt;# https://stackoverflow.com/questions/16553089/dynamic-variable-names-in-bash&lt;/span&gt;
&lt;span class="k"&gt;done&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  C. Setup the function and variables.
&lt;/h3&gt;

&lt;p&gt;Create a &lt;code&gt;handle_repo&lt;/code&gt; function and setup the variables you'll need. Note that the slackbot branch name must be url-escaped for the GitLab API calls:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;handle_repo &lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;

  &lt;span class="c"&gt;#1. Set up variables.&lt;/span&gt;
  &lt;span class="nb"&gt;local &lt;/span&gt;&lt;span class="nv"&gt;REPO&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nv"&gt;$1&lt;/span&gt; &lt;span class="c"&gt;# first function argument&lt;/span&gt;
  &lt;span class="nb"&gt;local &lt;/span&gt;&lt;span class="nv"&gt;PROJECT_ID&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nv"&gt;$2&lt;/span&gt; &lt;span class="c"&gt;# second function argument&lt;/span&gt;
  &lt;span class="nb"&gt;local &lt;/span&gt;&lt;span class="nv"&gt;SOURCE_BRANCH_DISPLAY&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;3&lt;/span&gt;&lt;span class="k"&gt;:-&lt;/span&gt;&lt;span class="s2"&gt;"QA"&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt; &lt;span class="c"&gt;# default value of QA&lt;/span&gt;
  &lt;span class="nb"&gt;local &lt;/span&gt;&lt;span class="nv"&gt;TARGET_BRANCH_DISPLAY&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;4&lt;/span&gt;&lt;span class="k"&gt;:-&lt;/span&gt;&lt;span class="s2"&gt;"Dev"&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt; &lt;span class="c"&gt;# default value of Dev&lt;/span&gt;

  &lt;span class="c"&gt;# Use exact (lowercase) branch name for Gitlab API calls&lt;/span&gt;
  &lt;span class="nb"&gt;local &lt;/span&gt;&lt;span class="nv"&gt;SOURCE_BRANCH&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$SOURCE_BRANCH_DISPLAY&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; | &lt;span class="nb"&gt;tr&lt;/span&gt; &lt;span class="s1"&gt;'[:upper:]'&lt;/span&gt; &lt;span class="s1"&gt;'[:lower:]'&lt;/span&gt; &lt;span class="si"&gt;)&lt;/span&gt;
  &lt;span class="nb"&gt;local &lt;/span&gt;&lt;span class="nv"&gt;TARGET_BRANCH&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$TARGET_BRANCH_DISPLAY&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; | &lt;span class="nb"&gt;tr&lt;/span&gt; &lt;span class="s1"&gt;'[:upper:]'&lt;/span&gt; &lt;span class="s1"&gt;'[:lower:]'&lt;/span&gt; &lt;span class="si"&gt;)&lt;/span&gt;

  &lt;span class="nb"&gt;local &lt;/span&gt;&lt;span class="nv"&gt;MR_TITLE&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;SOURCE_BRANCH_DISPLAY&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; =&amp;gt; &lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;TARGET_BRANCH_DISPLAY&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;

  &lt;span class="nb"&gt;local &lt;/span&gt;&lt;span class="nv"&gt;SLACKBOT_BRANCH&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"slackbot%2fqa-to-dev"&lt;/span&gt; &lt;span class="c"&gt;# URL escaped for API calls&lt;/span&gt;
  &lt;span class="nb"&gt;local &lt;/span&gt;&lt;span class="nv"&gt;SLACKBOT_BRANCH_DISPLAY&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"slackbot/qa-to-dev"&lt;/span&gt;

  &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="nt"&gt;-z&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$REPO&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;]&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="nt"&gt;-z&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$SOURCE_BRANCH&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;]&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="nt"&gt;-z&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$TARGET_BRANCH&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;]&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="nt"&gt;-z&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$PROJECT_ID&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;]&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;  &lt;span class="k"&gt;then
    &lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"Missing environment variables: REPO: &lt;/span&gt;&lt;span class="nv"&gt;$REPO&lt;/span&gt;&lt;span class="s2"&gt;, SOURCE_BRANCH: &lt;/span&gt;&lt;span class="nv"&gt;$SOURCE_BRANCH&lt;/span&gt;&lt;span class="s2"&gt;, TARGET_BRANCH: &lt;/span&gt;&lt;span class="nv"&gt;$TARGET_BRANCH&lt;/span&gt;&lt;span class="s2"&gt;, PROJECT_ID: &lt;/span&gt;&lt;span class="nv"&gt;$PROJECT_ID&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
    &lt;span class="k"&gt;return &lt;/span&gt;1
  &lt;span class="k"&gt;fi

   &lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"Running handle_repo for &lt;/span&gt;&lt;span class="nv"&gt;$REPO&lt;/span&gt;&lt;span class="s2"&gt;: From &lt;/span&gt;&lt;span class="nv"&gt;$SOURCE_BRANCH&lt;/span&gt;&lt;span class="s2"&gt; to &lt;/span&gt;&lt;span class="nv"&gt;$TARGET_BRANCH&lt;/span&gt;&lt;span class="s2"&gt; for project &lt;/span&gt;&lt;span class="nv"&gt;$PROJECT_ID&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  D. Start with a clean slate.
&lt;/h3&gt;

&lt;p&gt;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 &lt;code&gt;slackbot/qa-to-dev&lt;/code&gt; branch if it exists.  This (usually) will delete the old MR too.  Then we'll create a new &lt;code&gt;slackbot/qa-to-dev&lt;/code&gt; branch off of QA.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;#2. Delete existing slackbot branch&lt;/span&gt;
&lt;span class="nv"&gt;DELETE_BRANCH_RESP&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;curl &lt;span class="nt"&gt;-X&lt;/span&gt; DELETE &lt;span class="nt"&gt;--header&lt;/span&gt; &lt;span class="s2"&gt;"PRIVATE-TOKEN: &lt;/span&gt;&lt;span class="nv"&gt;$GITLAB_TOKEN&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="s2"&gt;"https://gitlab.com/api/v4/projects/&lt;/span&gt;&lt;span class="nv"&gt;$PROJECT_ID&lt;/span&gt;&lt;span class="s2"&gt;/repository/branches/&lt;/span&gt;&lt;span class="nv"&gt;$SLACKBOT_BRANCH&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;
&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"delete branch resp is &lt;/span&gt;&lt;span class="nv"&gt;$DELETE_BRANCH_RESP&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;

&lt;span class="c"&gt;#3. Create remote slackbot branch off of source branch&lt;/span&gt;
&lt;span class="nv"&gt;CREATE_BRANCH_RESP&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;curl &lt;span class="nt"&gt;-X&lt;/span&gt; POST &lt;span class="nt"&gt;--header&lt;/span&gt; &lt;span class="s2"&gt;"PRIVATE-TOKEN: &lt;/span&gt;&lt;span class="nv"&gt;$GITLAB_TOKEN&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="s2"&gt;"https://gitlab.com/api/v4/projects/&lt;/span&gt;&lt;span class="nv"&gt;$PROJECT_ID&lt;/span&gt;&lt;span class="s2"&gt;/repository/branches?branch=&lt;/span&gt;&lt;span class="nv"&gt;$SLACKBOT_BRANCH&lt;/span&gt;&lt;span class="s2"&gt;&amp;amp;ref=&lt;/span&gt;&lt;span class="nv"&gt;$SOURCE_BRANCH&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;
&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"create branch resp is &lt;/span&gt;&lt;span class="nv"&gt;$CREATE_BRANCH_RESP&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  E. Create the MR and get MR info.
&lt;/h3&gt;

&lt;p&gt;We'll create a new MR from &lt;code&gt;slackbot/qa-to-dev&lt;/code&gt; to Dev using the GitLab API, and get the MR ID from the response.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;#4. Create merge request to target branch&lt;/span&gt;
&lt;span class="nv"&gt;CREATE_MR_RESP&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;curl &lt;span class="nt"&gt;-X&lt;/span&gt; POST &lt;span class="nt"&gt;--header&lt;/span&gt; &lt;span class="s2"&gt;"PRIVATE-TOKEN: &lt;/span&gt;&lt;span class="nv"&gt;$GITLAB_TOKEN&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="s2"&gt;"https://gitlab.com/api/v4/projects/&lt;/span&gt;&lt;span class="nv"&gt;$PROJECT_ID&lt;/span&gt;&lt;span class="s2"&gt;/merge_requests?source_branch=&lt;/span&gt;&lt;span class="nv"&gt;$SLACKBOT_BRANCH&lt;/span&gt;&lt;span class="s2"&gt;&amp;amp;target_branch=&lt;/span&gt;&lt;span class="nv"&gt;$TARGET_BRANCH&lt;/span&gt;&lt;span class="s2"&gt;&amp;amp;simple=true"&lt;/span&gt; &lt;span class="nt"&gt;--data-urlencode&lt;/span&gt; &lt;span class="s2"&gt;"title=&lt;/span&gt;&lt;span class="nv"&gt;$MR_TITLE&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;
&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"create MR resp is &lt;/span&gt;&lt;span class="nv"&gt;$CREATE_MR_RESP&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;

&lt;span class="c"&gt;# Handle Gitlab occasionally retaining yesterday's unmerged MR after delete/create&lt;/span&gt;
&lt;span class="nv"&gt;ALREADY_EXISTS&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$CREATE_MR_RESP&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="s1"&gt;'already exists'&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;

&lt;span class="c"&gt;# 5. Get MR ID&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="nt"&gt;-n&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$ALREADY_EXISTS&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;]&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then
  &lt;/span&gt;&lt;span class="nv"&gt;MR_ID&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;CREATE_MR_RESP&lt;/span&gt;&lt;span class="p"&gt;//[^0-9]/&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="c"&gt;# extract numbers from response string&lt;/span&gt;
  &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"MR already exists &lt;/span&gt;&lt;span class="nv"&gt;$MR_ID&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;span class="k"&gt;else
  &lt;/span&gt;&lt;span class="nv"&gt;MR_ID&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$CREATE_MR_RESP&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; | python3 &lt;span class="nt"&gt;-c&lt;/span&gt; &lt;span class="s2"&gt;"import sys, json; print(json.load(sys.stdin)['iid'])"&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;fi&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then, we'll get the MR's info from the GitLab API. I've found we need to &lt;code&gt;sleep&lt;/code&gt; for 10 seconds before fetching the MR info to let GitLab finish calculating merge conflict info.&lt;/p&gt;

&lt;p&gt;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.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sleep &lt;/span&gt;10  &lt;span class="c"&gt;# Wait for GitLab to calculate merge conflicts info&lt;/span&gt;

&lt;span class="nv"&gt;MR_INFO_RESP&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;curl &lt;span class="nt"&gt;--header&lt;/span&gt; &lt;span class="s2"&gt;"PRIVATE-TOKEN: &lt;/span&gt;&lt;span class="nv"&gt;$GITLAB_TOKEN&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="s2"&gt;"https://gitlab.com/api/v4/projects/&lt;/span&gt;&lt;span class="nv"&gt;$PROJECT_ID&lt;/span&gt;&lt;span class="s2"&gt;/merge_requests/&lt;/span&gt;&lt;span class="nv"&gt;$MR_ID&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;
&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"MR info resp is &lt;/span&gt;&lt;span class="nv"&gt;$MR_INFO_RESP&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;

&lt;span class="c"&gt;# 7. Get MR URL&lt;/span&gt;
&lt;span class="nv"&gt;URL&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$MR_INFO_RESP&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; | python3 &lt;span class="nt"&gt;-c&lt;/span&gt; &lt;span class="s2"&gt;"import sys, json; print(json.load(sys.stdin)['web_url'])"&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;

&lt;span class="c"&gt;# 8. Set initial slack message&lt;/span&gt;
&lt;span class="nv"&gt;MSG&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$REPO&lt;/span&gt;&lt;span class="s2"&gt;: &lt;/span&gt;&lt;span class="nv"&gt;$MR_TITLE&lt;/span&gt;&lt;span class="s2"&gt;: &lt;/span&gt;&lt;span class="nv"&gt;$URL&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"New MR: &lt;/span&gt;&lt;span class="nv"&gt;$MSG&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  F. Report on merge conflicts.
&lt;/h3&gt;

&lt;p&gt;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 &lt;code&gt;slackbot/qa-to-dev&lt;/code&gt;, merge Dev into it locally, resolve the conflicts, then push back up. That way, no one has to create a new branch.&lt;/p&gt;

&lt;p&gt;Note that we can use Slack text formatting and emojis in our message. 🎉&lt;/p&gt;

&lt;p&gt;Also note that when &lt;a href="https://stackoverflow.com/questions/1955505/parsing-json-with-unix-tools"&gt;using Python to parse JSON&lt;/a&gt;, &lt;code&gt;true&lt;/code&gt; values become uppercase &lt;code&gt;True&lt;/code&gt;, and &lt;code&gt;null&lt;/code&gt; values become &lt;code&gt;None&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# 9. Report on merge conflicts&lt;/span&gt;
&lt;span class="nv"&gt;CONFLICTS&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$MR_INFO_RESP&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; | python3 &lt;span class="nt"&gt;-c&lt;/span&gt; &lt;span class="s2"&gt;"import sys, json; print(json.load(sys.stdin)['has_conflicts'])"&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$CONFLICTS&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="s1"&gt;'True'&lt;/span&gt; &lt;span class="o"&gt;]&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then
  &lt;/span&gt;&lt;span class="nv"&gt;MSG&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$MSG&lt;/span&gt;&lt;span class="s2"&gt;. *Merge Conflicts!* 🙀 _Pull down &lt;/span&gt;&lt;span class="nv"&gt;$SLACKBOT_BRANCH_DISPLAY&lt;/span&gt;&lt;span class="s2"&gt;, merge &lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;TARGET_BRANCH&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; into it locally, resolve conflicts, then push back up._ 😸"&lt;/span&gt;
&lt;span class="k"&gt;fi&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  G. Close empty MRs.
&lt;/h3&gt;

&lt;p&gt;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 &lt;code&gt;MSG&lt;/code&gt; variable to let the team know the branches are already synced for that repo.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# 10. If MR is empty, close it&lt;/span&gt;
&lt;span class="nv"&gt;CHANGES&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$MR_INFO_RESP&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; | python3 &lt;span class="nt"&gt;-c&lt;/span&gt; &lt;span class="s2"&gt;"import sys, json; print(json.load(sys.stdin)['changes_count'])"&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;
&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"changes count is &lt;/span&gt;&lt;span class="nv"&gt;$CHANGES&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;

&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$CHANGES&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="s2"&gt;"None"&lt;/span&gt; &lt;span class="o"&gt;]&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then
  &lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"No changes, closing MR"&lt;/span&gt;
  &lt;span class="nv"&gt;MSG&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$REPO&lt;/span&gt;&lt;span class="s2"&gt;: &lt;/span&gt;&lt;span class="nv"&gt;$SOURCE_BRANCH_DISPLAY&lt;/span&gt;&lt;span class="s2"&gt; is synced with &lt;/span&gt;&lt;span class="nv"&gt;$TARGET_BRANCH_DISPLAY&lt;/span&gt;&lt;span class="s2"&gt;! 🎉"&lt;/span&gt;
  curl &lt;span class="nt"&gt;-X&lt;/span&gt; PUT &lt;span class="nt"&gt;--header&lt;/span&gt; &lt;span class="s2"&gt;"PRIVATE-TOKEN: &lt;/span&gt;&lt;span class="nv"&gt;$GITLAB_TOKEN&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="s2"&gt;"https://gitlab.com/api/v4/projects/&lt;/span&gt;&lt;span class="nv"&gt;$PROJECT_ID&lt;/span&gt;&lt;span class="s2"&gt;/merge_requests/&lt;/span&gt;&lt;span class="nv"&gt;$MR_ID&lt;/span&gt;&lt;span class="s2"&gt;?state_event=close"&lt;/span&gt;
  &lt;span class="k"&gt;fi&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  H. Send to Slack!
&lt;/h3&gt;

&lt;p&gt;Finally, we're ready to send our message to Slack.  We'll simply send JSON to our webhook, with a key of &lt;code&gt;text&lt;/code&gt; pointing to our &lt;code&gt;MSG&lt;/code&gt; variable that we set above, escaping the interior quotation marks.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# 11. Send to Slack&lt;/span&gt;
curl &lt;span class="nt"&gt;-X&lt;/span&gt; POST &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s1"&gt;'Content-type: application/json'&lt;/span&gt; &lt;span class="nt"&gt;--data&lt;/span&gt; &lt;span class="s2"&gt;"{&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;text&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;:&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="nv"&gt;$MSG&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;}"&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$SLACK_URL&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  I. Test Locally
&lt;/h3&gt;

&lt;p&gt;That's the script!  We can try running it locally to send a test DM to ourselves and make sure everything works.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. Setup the Scheduled Job
&lt;/h2&gt;

&lt;p&gt;Almost there!  All that's left is to configure the scheduled job on GitLab.&lt;/p&gt;

&lt;p&gt;On the GitLab menu in your project, navigate to CI/CD -&amp;gt; Schedules, and click "Create New Schedule".&lt;/p&gt;

&lt;p&gt;Set the cron signature and timezone. I set the job to run every weekday at 9am Eastern with this cron signature: &lt;code&gt;0 9 * * 1-5&lt;/code&gt;.  (Checkout &lt;a href="https://crontab.guru/"&gt;crontab.guru&lt;/a&gt; to test out cron signatures.)&lt;/p&gt;

&lt;p&gt;Set the target branch to the branch in which you wrote the slackbot script.  (E.g., &lt;code&gt;js/slackbot&lt;/code&gt;, &lt;em&gt;not&lt;/em&gt; the &lt;code&gt;$SLACKBOT_BRANCH&lt;/code&gt; that the slackbot will use to create MRs!)&lt;/p&gt;

&lt;p&gt;Enter in your environment variables, set the job to activated, and click save.&lt;/p&gt;

&lt;p&gt;I'd recommend testing it out first by setting the &lt;code&gt;SLACK_URL&lt;/code&gt; variable to send you a DM, and clicking the play button on the job. If all goes well, switch the &lt;code&gt;SLACK_URL&lt;/code&gt; to send the message to your team's channel.&lt;/p&gt;

&lt;p&gt;And TADA!  You've got yourself a slackbot.&lt;/p&gt;

&lt;h2&gt;
  
  
  Further Enhancements
&lt;/h2&gt;

&lt;p&gt;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 over to Node.&lt;/p&gt;

&lt;h1&gt;
  
  
  Conclusion
&lt;/h1&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;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 and improve your team's experience.&lt;/p&gt;

&lt;p&gt;Happy coding!&lt;/p&gt;

&lt;h1&gt;
  
  
  Full Slackbot Script
&lt;/h1&gt;

&lt;p&gt;Also available at this &lt;a href="https://gist.github.com/Josh-Stillman/4dd0388c96868669ee7b6e123a9c37e5"&gt;gist&lt;/a&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;#!/usr/bin/env bash&lt;/span&gt;

&lt;span class="c"&gt;# Function that will be called for each repo.  Script exectuion begins below.&lt;/span&gt;
handle_repo &lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;

  &lt;span class="c"&gt;#1. Set up variables.&lt;/span&gt;
  &lt;span class="nb"&gt;local &lt;/span&gt;&lt;span class="nv"&gt;REPO&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nv"&gt;$1&lt;/span&gt; &lt;span class="c"&gt;# first function argument&lt;/span&gt;
  &lt;span class="nb"&gt;local &lt;/span&gt;&lt;span class="nv"&gt;PROJECT_ID&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nv"&gt;$2&lt;/span&gt; &lt;span class="c"&gt;# second function argument&lt;/span&gt;
  &lt;span class="nb"&gt;local &lt;/span&gt;&lt;span class="nv"&gt;SOURCE_BRANCH_DISPLAY&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;3&lt;/span&gt;&lt;span class="k"&gt;:-&lt;/span&gt;&lt;span class="s2"&gt;"QA"&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt; &lt;span class="c"&gt;# default value&lt;/span&gt;
  &lt;span class="nb"&gt;local &lt;/span&gt;&lt;span class="nv"&gt;TARGET_BRANCH_DISPLAY&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;4&lt;/span&gt;&lt;span class="k"&gt;:-&lt;/span&gt;&lt;span class="s2"&gt;"Dev"&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt; &lt;span class="c"&gt;# default value&lt;/span&gt;

  &lt;span class="c"&gt;# Use exact (lowercase) branch name for Gitlab API calls&lt;/span&gt;
  &lt;span class="nb"&gt;local &lt;/span&gt;&lt;span class="nv"&gt;SOURCE_BRANCH&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$SOURCE_BRANCH_DISPLAY&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; | &lt;span class="nb"&gt;tr&lt;/span&gt; &lt;span class="s1"&gt;'[:upper:]'&lt;/span&gt; &lt;span class="s1"&gt;'[:lower:]'&lt;/span&gt; &lt;span class="si"&gt;)&lt;/span&gt;
  &lt;span class="nb"&gt;local &lt;/span&gt;&lt;span class="nv"&gt;TARGET_BRANCH&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$TARGET_BRANCH_DISPLAY&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; | &lt;span class="nb"&gt;tr&lt;/span&gt; &lt;span class="s1"&gt;'[:upper:]'&lt;/span&gt; &lt;span class="s1"&gt;'[:lower:]'&lt;/span&gt; &lt;span class="si"&gt;)&lt;/span&gt;

  &lt;span class="nb"&gt;local &lt;/span&gt;&lt;span class="nv"&gt;MR_TITLE&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;SOURCE_BRANCH_DISPLAY&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; =&amp;gt; &lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;TARGET_BRANCH_DISPLAY&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;

  &lt;span class="nb"&gt;local &lt;/span&gt;&lt;span class="nv"&gt;SLACKBOT_BRANCH&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"slackbot%2fqa-to-dev"&lt;/span&gt; &lt;span class="c"&gt;# URL escaped for API calls&lt;/span&gt;
  &lt;span class="nb"&gt;local &lt;/span&gt;&lt;span class="nv"&gt;SLACKBOT_BRANCH_DISPLAY&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"slackbot/qa-to-dev"&lt;/span&gt;

  &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="nt"&gt;-z&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$REPO&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;]&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="nt"&gt;-z&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$SOURCE_BRANCH&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;]&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="nt"&gt;-z&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$TARGET_BRANCH&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;]&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="nt"&gt;-z&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$PROJECT_ID&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;]&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;  &lt;span class="k"&gt;then
    &lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"Missing environment variables: REPO: &lt;/span&gt;&lt;span class="nv"&gt;$REPO&lt;/span&gt;&lt;span class="s2"&gt;, SOURCE_BRANCH: &lt;/span&gt;&lt;span class="nv"&gt;$SOURCE_BRANCH&lt;/span&gt;&lt;span class="s2"&gt;, TARGET_BRANCH: &lt;/span&gt;&lt;span class="nv"&gt;$TARGET_BRANCH&lt;/span&gt;&lt;span class="s2"&gt;, PROJECT_ID: &lt;/span&gt;&lt;span class="nv"&gt;$PROJECT_ID&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
    &lt;span class="k"&gt;return &lt;/span&gt;1
  &lt;span class="k"&gt;fi

  &lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"Running handle_repo for &lt;/span&gt;&lt;span class="nv"&gt;$REPO&lt;/span&gt;&lt;span class="s2"&gt;: From &lt;/span&gt;&lt;span class="nv"&gt;$SOURCE_BRANCH&lt;/span&gt;&lt;span class="s2"&gt; to &lt;/span&gt;&lt;span class="nv"&gt;$TARGET_BRANCH&lt;/span&gt;&lt;span class="s2"&gt; for project &lt;/span&gt;&lt;span class="nv"&gt;$PROJECT_ID&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;

  &lt;span class="c"&gt;#2. Delete existing slackbot branch&lt;/span&gt;
  &lt;span class="nv"&gt;DELETE_BRANCH_RESP&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;curl &lt;span class="nt"&gt;-X&lt;/span&gt; DELETE &lt;span class="nt"&gt;--header&lt;/span&gt; &lt;span class="s2"&gt;"PRIVATE-TOKEN: &lt;/span&gt;&lt;span class="nv"&gt;$GITLAB_TOKEN&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="s2"&gt;"https://gitlab.com/api/v4/projects/&lt;/span&gt;&lt;span class="nv"&gt;$PROJECT_ID&lt;/span&gt;&lt;span class="s2"&gt;/repository/branches/&lt;/span&gt;&lt;span class="nv"&gt;$SLACKBOT_BRANCH&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;
  &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"delete branch resp is &lt;/span&gt;&lt;span class="nv"&gt;$DELETE_BRANCH_RESP&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;

  &lt;span class="c"&gt;#3. Create remote slackbot branch off of source branch&lt;/span&gt;
  &lt;span class="nv"&gt;CREATE_BRANCH_RESP&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;curl &lt;span class="nt"&gt;-X&lt;/span&gt; POST &lt;span class="nt"&gt;--header&lt;/span&gt; &lt;span class="s2"&gt;"PRIVATE-TOKEN: &lt;/span&gt;&lt;span class="nv"&gt;$GITLAB_TOKEN&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="s2"&gt;"https://gitlab.com/api/v4/projects/&lt;/span&gt;&lt;span class="nv"&gt;$PROJECT_ID&lt;/span&gt;&lt;span class="s2"&gt;/repository/branches?branch=&lt;/span&gt;&lt;span class="nv"&gt;$SLACKBOT_BRANCH&lt;/span&gt;&lt;span class="s2"&gt;&amp;amp;ref=&lt;/span&gt;&lt;span class="nv"&gt;$SOURCE_BRANCH&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;
  &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"create branch resp is &lt;/span&gt;&lt;span class="nv"&gt;$CREATE_BRANCH_RESP&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;

  &lt;span class="c"&gt;#4. Create merge request to target branch&lt;/span&gt;
  &lt;span class="nv"&gt;CREATE_MR_RESP&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;curl &lt;span class="nt"&gt;-X&lt;/span&gt; POST &lt;span class="nt"&gt;--header&lt;/span&gt; &lt;span class="s2"&gt;"PRIVATE-TOKEN: &lt;/span&gt;&lt;span class="nv"&gt;$GITLAB_TOKEN&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="s2"&gt;"https://gitlab.com/api/v4/projects/&lt;/span&gt;&lt;span class="nv"&gt;$PROJECT_ID&lt;/span&gt;&lt;span class="s2"&gt;/merge_requests?source_branch=&lt;/span&gt;&lt;span class="nv"&gt;$SLACKBOT_BRANCH&lt;/span&gt;&lt;span class="s2"&gt;&amp;amp;target_branch=&lt;/span&gt;&lt;span class="nv"&gt;$TARGET_BRANCH&lt;/span&gt;&lt;span class="s2"&gt;&amp;amp;simple=true"&lt;/span&gt; &lt;span class="nt"&gt;--data-urlencode&lt;/span&gt; &lt;span class="s2"&gt;"title=&lt;/span&gt;&lt;span class="nv"&gt;$MR_TITLE&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;
  &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"create MR resp is &lt;/span&gt;&lt;span class="nv"&gt;$CREATE_MR_RESP&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;

  &lt;span class="c"&gt;# Handle Gitlab occasionally retaining yesterday's unmerged MR after delete/create&lt;/span&gt;
  &lt;span class="nv"&gt;ALREADY_EXISTS&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$CREATE_MR_RESP&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="s1"&gt;'already exists'&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;

  &lt;span class="c"&gt;# 5. Get MR ID&lt;/span&gt;
  &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="nt"&gt;-n&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$ALREADY_EXISTS&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;]&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then
    &lt;/span&gt;&lt;span class="nv"&gt;MR_ID&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;CREATE_MR_RESP&lt;/span&gt;&lt;span class="p"&gt;//[^0-9]/&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="c"&gt;# extract numbers from response string&lt;/span&gt;
    &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"MR already exists &lt;/span&gt;&lt;span class="nv"&gt;$MR_ID&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
  &lt;span class="k"&gt;else
    &lt;/span&gt;&lt;span class="nv"&gt;MR_ID&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$CREATE_MR_RESP&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; | python3 &lt;span class="nt"&gt;-c&lt;/span&gt; &lt;span class="s2"&gt;"import sys, json; print(json.load(sys.stdin)['iid'])"&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;
  &lt;span class="k"&gt;fi&lt;/span&gt;

  &lt;span class="c"&gt;# 6. Get MR info from GitLab API&lt;/span&gt;
  &lt;span class="nb"&gt;sleep &lt;/span&gt;10  &lt;span class="c"&gt;# Wait for GitLab to calculate merge conflicts info&lt;/span&gt;
  &lt;span class="nv"&gt;MR_INFO_RESP&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;curl &lt;span class="nt"&gt;--header&lt;/span&gt; &lt;span class="s2"&gt;"PRIVATE-TOKEN: &lt;/span&gt;&lt;span class="nv"&gt;$GITLAB_TOKEN&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="s2"&gt;"https://gitlab.com/api/v4/projects/&lt;/span&gt;&lt;span class="nv"&gt;$PROJECT_ID&lt;/span&gt;&lt;span class="s2"&gt;/merge_requests/&lt;/span&gt;&lt;span class="nv"&gt;$MR_ID&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;
  &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"MR info resp is &lt;/span&gt;&lt;span class="nv"&gt;$MR_INFO_RESP&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;

  &lt;span class="c"&gt;# 7. Get MR URL&lt;/span&gt;
  &lt;span class="nv"&gt;URL&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$MR_INFO_RESP&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; | python3 &lt;span class="nt"&gt;-c&lt;/span&gt; &lt;span class="s2"&gt;"import sys, json; print(json.load(sys.stdin)['web_url'])"&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;

  &lt;span class="c"&gt;# 8. Set initial slack message&lt;/span&gt;
  &lt;span class="nv"&gt;MSG&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$REPO&lt;/span&gt;&lt;span class="s2"&gt;: &lt;/span&gt;&lt;span class="nv"&gt;$MR_TITLE&lt;/span&gt;&lt;span class="s2"&gt;: &lt;/span&gt;&lt;span class="nv"&gt;$URL&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
  &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"New MR: &lt;/span&gt;&lt;span class="nv"&gt;$MSG&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;

  &lt;span class="c"&gt;# 9. Report on merge conflicts&lt;/span&gt;
  &lt;span class="nv"&gt;CONFLICTS&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$MR_INFO_RESP&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; | python3 &lt;span class="nt"&gt;-c&lt;/span&gt; &lt;span class="s2"&gt;"import sys, json; print(json.load(sys.stdin)['has_conflicts'])"&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;

  &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$CONFLICTS&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="s1"&gt;'True'&lt;/span&gt; &lt;span class="o"&gt;]&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then
    &lt;/span&gt;&lt;span class="nv"&gt;MSG&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$MSG&lt;/span&gt;&lt;span class="s2"&gt;. *Merge Conflicts!* 🙀 _Pull down &lt;/span&gt;&lt;span class="nv"&gt;$SLACKBOT_BRANCH_DISPLAY&lt;/span&gt;&lt;span class="s2"&gt;, merge &lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;TARGET_BRANCH&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; into it locally, resolve conflicts, then push back up._ 😸"&lt;/span&gt;
  &lt;span class="k"&gt;fi&lt;/span&gt;

  &lt;span class="c"&gt;# 10. If MR is empty, close it&lt;/span&gt;
  &lt;span class="nv"&gt;CHANGES&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$MR_INFO_RESP&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; | python3 &lt;span class="nt"&gt;-c&lt;/span&gt; &lt;span class="s2"&gt;"import sys, json; print(json.load(sys.stdin)['changes_count'])"&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;
  &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"changes count is &lt;/span&gt;&lt;span class="nv"&gt;$CHANGES&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;

  &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$CHANGES&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="s2"&gt;"None"&lt;/span&gt; &lt;span class="o"&gt;]&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then
    &lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"No changes, closing MR"&lt;/span&gt;
    &lt;span class="nv"&gt;MSG&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$REPO&lt;/span&gt;&lt;span class="s2"&gt;: &lt;/span&gt;&lt;span class="nv"&gt;$SOURCE_BRANCH_DISPLAY&lt;/span&gt;&lt;span class="s2"&gt; is synced with &lt;/span&gt;&lt;span class="nv"&gt;$TARGET_BRANCH_DISPLAY&lt;/span&gt;&lt;span class="s2"&gt;! 🎉"&lt;/span&gt;
    curl &lt;span class="nt"&gt;-X&lt;/span&gt; PUT &lt;span class="nt"&gt;--header&lt;/span&gt; &lt;span class="s2"&gt;"PRIVATE-TOKEN: &lt;/span&gt;&lt;span class="nv"&gt;$GITLAB_TOKEN&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="s2"&gt;"https://gitlab.com/api/v4/projects/&lt;/span&gt;&lt;span class="nv"&gt;$PROJECT_ID&lt;/span&gt;&lt;span class="s2"&gt;/merge_requests/&lt;/span&gt;&lt;span class="nv"&gt;$MR_ID&lt;/span&gt;&lt;span class="s2"&gt;?state_event=close"&lt;/span&gt;
  &lt;span class="k"&gt;fi&lt;/span&gt;

  &lt;span class="c"&gt;# 11. Send to Slack&lt;/span&gt;
  curl &lt;span class="nt"&gt;-X&lt;/span&gt; POST &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s1"&gt;'Content-type: application/json'&lt;/span&gt; &lt;span class="nt"&gt;--data&lt;/span&gt; &lt;span class="s2"&gt;"{&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;text&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;:&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="nv"&gt;$MSG&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;}"&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$SLACK_URL&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;

&lt;span class="c"&gt;# Begin script execution here:&lt;/span&gt;

&lt;span class="c"&gt;# Setup env vars.  Located in the Gitlab project settings console.  For running locally, use a .env file at project root and export the vars.&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="nt"&gt;-f&lt;/span&gt; &lt;span class="s2"&gt;"./.env"&lt;/span&gt; &lt;span class="o"&gt;]&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then
    &lt;/span&gt;&lt;span class="nb"&gt;source&lt;/span&gt; ./.env
    &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"Importing env vars from .env file"&lt;/span&gt;
&lt;span class="k"&gt;fi

if&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="nt"&gt;-z&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$SLACK_URL&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;]&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="nt"&gt;-z&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$GITLAB_TOKEN&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;]&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="nt"&gt;-z&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$REPO_DISPLAY_NAMES&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;]&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;  &lt;span class="k"&gt;then
  &lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"Missing environment Variables: SLACK_URL: &lt;/span&gt;&lt;span class="nv"&gt;$SLACK_URL&lt;/span&gt;&lt;span class="s2"&gt;, GITLAB_TOKEN: &lt;/span&gt;&lt;span class="nv"&gt;$GITLAB_TOKEN&lt;/span&gt;&lt;span class="s2"&gt;, REPO_DISPLAY_NAMES: &lt;/span&gt;&lt;span class="nv"&gt;$REPO_DISPLAY_NAMES&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
  &lt;span class="nb"&gt;exit &lt;/span&gt;1
&lt;span class="k"&gt;fi&lt;/span&gt;

&lt;span class="c"&gt;# Create array from comma-separated string of repo names&lt;/span&gt;
&lt;span class="nv"&gt;IFS&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;', '&lt;/span&gt; &lt;span class="nb"&gt;read&lt;/span&gt; &lt;span class="nt"&gt;-r&lt;/span&gt; &lt;span class="nt"&gt;-a&lt;/span&gt; REPO_ARRAY &lt;span class="o"&gt;&amp;lt;&amp;lt;&amp;lt;&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$REPO_DISPLAY_NAMES&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;

&lt;span class="c"&gt;# Call function for each repo.  For each repo name in the REPO_ARRAY, a matching variable with the GitLab project ID named with the repo name in all caps followed by _ID is required&lt;/span&gt;
&lt;span class="k"&gt;for &lt;/span&gt;REPO &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;REPO_ARRAY&lt;/span&gt;&lt;span class="p"&gt;[@]&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;do
  &lt;/span&gt;&lt;span class="nv"&gt;REPO_ID&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$REPO&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; | &lt;span class="nb"&gt;tr&lt;/span&gt; &lt;span class="s1"&gt;'[:lower:]'&lt;/span&gt; &lt;span class="s1"&gt;'[:upper:]'&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;_ID"&lt;/span&gt; &lt;span class="c"&gt;# Capitalize repo display name and add "_ID" to get project id's variable name&lt;/span&gt;

  handle_repo &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$REPO&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="p"&gt;!REPO_ID&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="c"&gt;# Indirect variable. REPO_ID's value is another variable's name, such as FRONTEND_ID, and we access that second variable's value.&lt;/span&gt;
  &lt;span class="c"&gt;# https://stackoverflow.com/questions/16553089/dynamic-variable-names-in-bash&lt;/span&gt;
&lt;span class="k"&gt;done&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;(First published on &lt;a href="https://medium.com/giant-machines/slackbot-to-the-rescue-keeping-gitlab-branches-in-sync-with-a-slackbot-ff84f80eaf5f"&gt;Medium&lt;/a&gt;)&lt;/p&gt;

</description>
      <category>git</category>
      <category>slack</category>
      <category>gitlab</category>
      <category>bash</category>
    </item>
  </channel>
</rss>
