|
| 1 | +--- |
| 2 | +title:Delivering deployments | GitHub API |
| 3 | +--- |
| 4 | + |
| 5 | +#Delivering deployments |
| 6 | + |
| 7 | +* TOC |
| 8 | +{:toc} |
| 9 | + |
| 10 | +The[Deployments API][deploy API] provides your projects hosted on GitHub with |
| 11 | +the capability to launch them on a production server that you own. Combined with |
| 12 | +[the Status API][status API], you'll be able to coordinate your deployments |
| 13 | +the moment your code lands on`master`. |
| 14 | + |
| 15 | +This guide will use that API to demonstrate a setup that you can use. |
| 16 | +In our scenario, we will: |
| 17 | + |
| 18 | +* Merge a Pull Request |
| 19 | +* When the CI is finished, we'll set the Pull Request's status accordingly. |
| 20 | +* When the Pull Request is merged, we'll run our deployment to our server. |
| 21 | + |
| 22 | +Our CI system and host server will be figments of our imagination. They could be |
| 23 | +Heroku, Amazon, or something else entirely. The crux of this guide will be setting up |
| 24 | +and configuring the server managing the communication. |
| 25 | + |
| 26 | +If you haven't already, be sure to[download ngrok][ngrok], and learn how |
| 27 | +to[use it][using ngrok]. We find it to be a very useful tool for exposing local |
| 28 | +connections. |
| 29 | + |
| 30 | +Note: you can download the complete source code for this project |
| 31 | +[from the platform-samples repo][platform samples]. |
| 32 | + |
| 33 | +##Writing your server |
| 34 | + |
| 35 | +We'll write a quick Sinatra app to prove that our local connections are working. |
| 36 | +Let's start with this: |
| 37 | + |
| 38 | +#!ruby |
| 39 | +require 'sinatra' |
| 40 | +require 'json' |
| 41 | + |
| 42 | +post '/event_handler' do |
| 43 | + payload = JSON.parse(params[:payload]) |
| 44 | + "Well, it worked!" |
| 45 | +end |
| 46 | + |
| 47 | + |
| 48 | +(If you're unfamiliar with how Sinatra works, we recommend[reading the Sinatra guide][Sinatra].) |
| 49 | + |
| 50 | +Start this server up. By default, Sinatra starts on port`9393`, so you'll want |
| 51 | +to configure ngrok to start listening for that, too. |
| 52 | + |
| 53 | +In order for this server to work, we'll need to set a repository up with a webhook. |
| 54 | +The webhook should be configured to fire whenever a Pull Request is created, or merged. |
| 55 | +Go ahead and create a repository you're comfortable playing around in. Might we |
| 56 | +suggest[@octocat's Spoon/Knife repository](https://github.com/octocat/Spoon-Knife)? |
| 57 | +After that, you'll create a new webhook in your repository, feeding it the URL |
| 58 | +that ngrok gave you: |
| 59 | + |
| 60 | + |
| 61 | + |
| 62 | +Click**Update webhook**. You should see a body response of`Well, it worked!`. |
| 63 | +Great! Click on**Let me select individual events.**, and select the following: |
| 64 | + |
| 65 | +* Deployment |
| 66 | +* Deployment status |
| 67 | +* Pull Request |
| 68 | + |
| 69 | +These are the events GitHub will send to our server whenever the relevant action |
| 70 | +occurs. We'll configure our server to*just* handle when Pull Requests are merged |
| 71 | +right now: |
| 72 | + |
| 73 | +#!ruby |
| 74 | +post '/event_handler' do |
| 75 | + @payload = JSON.parse(params[:payload]) |
| 76 | + |
| 77 | + case request.env['HTTP_X_GITHUB_EVENT'] |
| 78 | + when "pull_request" |
| 79 | + if @payload["action"] == "closed" && @payload["pull_request"]["merged"] |
| 80 | + puts "A pull request was merged! A deployment should start now..." |
| 81 | + end |
| 82 | + end |
| 83 | +end |
| 84 | + |
| 85 | +What's going on? Every event that GitHub sends out attached a`X-GitHub-Event` |
| 86 | +HTTP header. We'll only care about the PR events for now. When a pull request is |
| 87 | +merged (its state is`closed`, and`merged` is`true`), we'll kick off a deployment. |
| 88 | + |
| 89 | +To test out this proof-of-concept, make some changes in a branch in your test |
| 90 | +repository, open a pull request, and merge it. Your server should respond accordingly! |
| 91 | + |
| 92 | +##Working with deployments |
| 93 | + |
| 94 | +With our server in place, the code being reviewed, and our pull request |
| 95 | +is merged, we want our project to be deployed to the production server. |
| 96 | + |
| 97 | +We'll start by modifying our event listener to process pull requests when they're |
| 98 | +merged, and start paying attention to deployments: |
| 99 | + |
| 100 | +#!ruby |
| 101 | +when "pull_request" |
| 102 | + if @payload["action"] == "closed" && @payload["pull_request"]["merged"] |
| 103 | + start_deployment(@payload["pull_request"]) |
| 104 | + end |
| 105 | +when "deployment" |
| 106 | + process_deployment(@payload) |
| 107 | +when "deployment_status" |
| 108 | + update_deployment_status |
| 109 | +end |
| 110 | + |
| 111 | +Based on the information from the pull request, we'll start by filling out the |
| 112 | +`start_deployment` method: |
| 113 | + |
| 114 | +#!ruby |
| 115 | +def start_deployment(pull_request) |
| 116 | + user = pull_request['user']['login'] |
| 117 | + payload = JSON.generate(:environment => 'production', :deploy_user => user) |
| 118 | + @client.create_deployment(pull_request['head']['repo']['full_name'], pull_request['head']['sha'], {:payload => payload, :description => "Deploying my sweet branch"}) |
| 119 | +end |
| 120 | + |
| 121 | +Deployments can have some metadata attached to them, in the form of a`payload` |
| 122 | +and a`description`. Although these values are optional, it's helpful to use |
| 123 | +for logging and representing information. |
| 124 | + |
| 125 | +When a new deployment is created, a completely separate event is trigged. That's |
| 126 | +why we have a new`switch` case in the event handler for`deployment`. You can |
| 127 | +use this information to be notified when a deployment has been triggered. |
| 128 | + |
| 129 | +Deployments can take a rather long time, so we'll want to listen for various events, |
| 130 | +such as when the deployment was created, and what state it's in. |
| 131 | + |
| 132 | +Let's simulate a deployment that does some work, and notice the effect it has on |
| 133 | +the output. First, let's complete our`process_deployment` method: |
| 134 | + |
| 135 | +#!ruby |
| 136 | +def process_deployment |
| 137 | + payload = JSON.parse(@payload['payload']) |
| 138 | + # you can send this information to your chat room, monitor, pager, e.t.c. |
| 139 | + puts "Processing '#{@payload['description']}' for #{payload['deploy_user']} to #{payload['environment']}" |
| 140 | + sleep 2 # simulate work |
| 141 | + @client.create_deployment_status("repos/#{@payload['repository']['full_name']}/deployments/#{@payload['id']}", 'pending') |
| 142 | + sleep 2 # simulate work |
| 143 | + @client.create_deployment_status("repos/#{@payload['repository']['full_name']}/deployments/#{@payload['id']}", 'success') |
| 144 | +end |
| 145 | + |
| 146 | +Finally, we'll simulate storing the status information as console output: |
| 147 | + |
| 148 | +#!ruby |
| 149 | +def update_deployment_status |
| 150 | + puts "Deployment status for #{@payload['id']} is #{@payload['state']}" |
| 151 | +end |
| 152 | + |
| 153 | +Let's break down what's going on. A new deployment is created by`start_deployment`, |
| 154 | +which triggers the`deployment` event. From there, we call`process_deployment` |
| 155 | +to simulate work that's going on. During that processing, we also make a call to |
| 156 | +`create_deployment_status`, which lets a receiver know what's going on, as we |
| 157 | +switch the status to`pending`. |
| 158 | + |
| 159 | +After the deployment is finished, we set the status to`success`. You'll notice |
| 160 | +that this pattern is the exact same as when we you your CI statuses. |
| 161 | + |
| 162 | +##Conclusion |
| 163 | + |
| 164 | +At GitHub, we've used a version of[Heaven][heaven] to manage |
| 165 | +our deployments for years. The basic flow is essentially the exact same as the |
| 166 | +server we've built above. At GitHub, we: |
| 167 | + |
| 168 | +* Wait for a response on the state of the CI |
| 169 | +* If the code is green, we merge the pull request |
| 170 | +* Heaven takes the merged code, and deploys it to our production servers |
| 171 | +* In the meantime, Heaven also notifies everyone about the build, via[Hubot][hubot] sitting in our chat rooms |
| 172 | + |
| 173 | +That's it! You don't need to build your own CI or deployment setup to use this example. |
| 174 | +You can always rely on[third-party services][integrations]. |
| 175 | + |
| 176 | +[deploy API]:/v3/repos/deployments/ |
| 177 | +[status API]:/guides/building-a-ci-server |
| 178 | +[ngrok]:https://ngrok.com/ |
| 179 | +[using ngrok]:/webhooks/configuring/#using-ngrok |
| 180 | +[platform samples]:https://github.com/github/platform-samples/tree/master/api/ruby/delivering-deployments |
| 181 | +[Sinatra]:http://www.sinatrarb.com/ |
| 182 | +[webhook]:/webhooks/ |
| 183 | +[octokit.rb]:https://github.com/octokit/octokit.rb |
| 184 | +[access token]:https://help.github.com/articles/creating-an-access-token-for-command-line-use |
| 185 | +[travis api]:https://api.travis-ci.org/docs/ |
| 186 | +[janky]:https://github.com/github/janky |
| 187 | +[heaven]:https://github.com/atmos/heaven |
| 188 | +[hubot]:https://github.com/github/hubot |
| 189 | +[integrations]:https://github.com/integrations |