In Progress
Unit 1, Lesson 1
In Progress

Github Flow Automation

One of the most exciting changes that has happened to the software industry since I began my career has been the advent of continuous integration. Seeing updates to production functionality tested, approved, and deployed in hours or even minutes was unimaginable back when I got started.

But a continuous integration discipline is only as good as the consistent habits a team implements. Without a well-understood and consistently implemented process, production quality can suffer. Fortunately, this is an area where automation can help. In today’s episode, guest chef Maikel Lammers demonstrates the “Danger” tool. He’ll show you how a few lines of code can help facilitate and enforce the release process a team has agreed on. Enjoy!

Video transcript & code

 

opening_slide.001 As most software engineering teams, my team is agile, or at least trying to be. One of the core ideas of being agile, is to continuously reflect on the process and improve. But reflection isn't much good if the changes don't stick. I'm convinced that the better you succeed in enforcing these adjustments, the better the changes will be adopted in the long run. This will help make the improvements more effective and sustainable. Today I'm going to show you a way to enforce adjustments your team wants to make to your development flow, using the Danger gem.

I want to start with a real example: A while ago, we shipped a critical bug to production. When talking about this issue in a retro, we realized that we had shipped this defective code, without having QA'd it first. It had somehow skipped a step in our process. In our retro we came to the decision, that as a default, all code would have to be reviewed by QA, before it was shipped. Here's where we started thinking about how to block a Pull Request from merging, as long as it wasn't QA'd. We tried out the Danger gem.

Here's what we want to achieve in the end: First, we create a Pull Request.

Once it is created, our continuous integration runs. If it shouldn't be merged - like in this case because it hasn't been approved by QA - the merging is blocked. Danger will show a comment in the PR, what exactly kept it from being merged.

Once a change happens that unblocks the PR - like in this case a member of QA saying that the PR can be shipped - the comment gets updated, and the PR can be merged. So how do we go about achieving that? You can tell Danger what to do in your Dangerfile. Generally you can tell Danger to add a message, a warning, or a failure to the PR.

Let's create a simple example Dangerfile: As message, I'm going to tell the user what PR he or she is working on. Note that I'm calling methods on the GitHub instance, that encapsulates all communication with the GitHub API. I'm also going to add a warning, because the PR is too big for my taste. In the end I'm going to add a failure as well.

Let's run this Dangerfile locally to see the result. As you can see the output contains our messages, warnings and errors. In a finished integration, this output will be shown as a Danger comment in your Pull Request.

So now let's tackle our problem at hand:


fail('Needs QA approval') unless approved_by_qa?

What we want to achieve is something like this. Fail with a message, unless it has been approved by QA.


class DangerCheck
  def approved_by_qa?
  end
end

danger_check = DangerCheck.new

fail('Needs QA approval') unless danger_check.approved_by_qa?

To make this easier readable and testable, we'll create a class DangerCheck. And we want to give it the approved_by_qa? method.


class DangerCheck
  attr_accessor :github
  private :github

  def initialize(github:)
    @github = github
  end

  def approved_by_qa?
  end
end

danger_check = DangerCheck.new(github: github)

fail('Needs QA approval') unless danger_check.approved_by_qa?

We'll instantiate it with the GitHub instance mentioned earlier, that is used by Danger.


class DangerCheck

  QA_TEAM = %w(maikel-lammers).freeze

  attr_accessor :github
  private :github

  def initialize(github:)
    @github = github
  end

  def approved_by_qa?
    # checking for :ship: or :+1: from QA
    comments_from_qa_team.any? { |x| x.match(/\u{1F6A2}|\u{1F44D}/) }
  end

  private

  def comments_from_qa_team
  end
end

danger_check = DangerCheck.new(github: github)

fail('Needs QA approval') unless danger_check.approved_by_qa?

Next, I'll add a trust-worthy QA team, and define the approved_by_qa? method to return true, if any member of this team commented with either a thumbs-up, or a ship.


class DangerCheck

  QA_TEAM = %w(maikel-lammers).freeze

  attr_accessor :github
  private :github

  def initialize(github:)
    @github = github
  end

  def approved_by_qa?
    # checking for :ship: or :+1: from QA
    comments_from_qa_team.any? { |x| x.match(/\u{1F6A2}|\u{1F44D}/) }
  end

  private

  def comments_from_qa_team
    pr_comments.reduce([]) do |comment_array, comment|
      next comment_array unless QA_TEAM.include?(comment[:user][:login])
      comment_array << comment[:body]
    end
  end

  def pr_comments
  end
end

danger_check = DangerCheck.new(github: github)

fail('Needs QA approval') unless danger_check.qa_approved?

Now we'll add some meat to the comments_from_qa_team method. We'll go through all comments of the PR and check the user that made the comment. We will return all comments, that were written by a member of the QA team.


class DangerCheck

  QA_TEAM = %w(maikel-lammers).freeze

  attr_accessor :github
  private :github

  def initialize(github:)
    @github = github
  end

  def approved_by_qa?
    # checking for :ship: or :+1: from QA
    comments_from_qa_team.any? { |x| x.match(/\u{1F6A2}|\u{1F44D}/) }
  end

  private

  def comments_from_qa_team
    pr_comments.reduce([]) do |comment_array, comment|
      next comment_array unless QA_TEAM.include?(comment[:user][:login])
      comment_array << comment[:body]
    end
  end

  def pr_comments
    github.api.issue_comments(repo, issue_number)
  end

  def repo
    github.pr_json[:base][:repo][:full_name]
  end

  def issue_number
    github.pr_json[:number]
  end
end

danger_check = DangerCheck.new(github: github)

fail('Needs QA approval') unless danger_check.approved_by_qa?

As a last step, let's fetch the actual information from GitHub using Danger. We will need both the repo name, and the issue number, which can be fetched from the pr_json hash. Last, but not least, we fetch all the comments in the PR from the GitHub API.

Now we can try it out on a Pull Request: In this PR there is no QA comment yet. As expected, our error message is returned.

Now we approve the PR, by commenting with a ship emoji and we try again: And as expected, Danger finds no errors anymore. The last thing you will need to set up now, to make it work like the initial demonstration, is a hook in your GitHub repository to trigger Danger - we use Jenkins for that - and our first danger check is ready to run.

happy_shipping.002 Thank you for watching, I hope it will help some of you improving your workflow! Happy shipping! :)

Responses