Buildkite -> Your Server -> Notifications Channel
When you run your builds on Buildkite, Buildkite can issue Webhooks that you could listen and send to your preferred notification channels.
You can choose what kind of events you want to receive on Buildkite’s settings page.
Notifications Channels: Slack, Basecamp, Custom Server, etc.
Let’s see how to implement this part.
Buildkite -> Your Server
# frozen_string_literal: true
require "json"
class BuildkiteWebhooksController < ApplicationController
skip_before_action :verify_authenticity_token, only: :create
before_action :verify_buildkite_request
def create
# This will call a background job
BuildkiteEvent.process(event: buildkite_event, body: body)
head :ok
rescue StandardError => exception
# Replace with your Exception Notification mechanism
Rails.logger.error(exception.message)
head :unprocessable_entity
end
private
# https://buildkite.com/organizations/-/services > Webhook to get the token
BUILDKITE_WEBHOOK_TOKEN = ENV.fetch("BUILDKITE_WEBHOOK_TOKEN", "developmentoken").freeze
private_constant :BUILDKITE_WEBHOOK_TOKEN
def verify_buildkite_request
unless request_from_buildkite?
raise "Could not verify Buildkite request. X-Buildkite-Request: '#{buildkite_request}'"
end
end
def buildkite_event
request.headers["X-Buildkite-Event"]
end
def body
JSON.parse(request.raw_post)
end
# Use secure_compare, constant-time comparison
def request_from_buildkite?
Rack::Utils.secure_compare(buildkite_token, BUILDKITE_WEBHOOK_TOKEN)
end
def buildkite_token
request.headers["X-Buildkite-Token"].to_s
end
def buildkite_request
request.headers["X-Buildkite-Request"]
end
end
and BuildkiteEvent
to dispatch the request to different background job:
# frozen_string_literal: true
class BuildkiteEvent
def self.process(event:, body:)
new(event, body).process
end
def initialize(event, body)
@event = event
@body = body
end
def process
return unless event.include?("build")
# Post to a different channel
if deploy?
BuildkiteDeployNotificationJob.perform_later(payload.build)
else
BuildkiteBuildNotificationJob.perform_later(payload.build)
end
end
private
attr_reader :event, :body
def deploy?
body["pipeline"]["slug"] == "deploy"
end
def payload
BuildkitePayload.new(body)
end
end
and BuildkitePayload
:
# frozen_string_literal: true
class BuildkitePayload
def initialize(data)
@data = data
end
def build
_build = {
"id" => build_hash["id"],
"url" => build_hash["url"],
"web_url" => build_hash["web_url"],
"number" => build_hash["number"],
"state" => build_hash["state"],
"blocked" => build_hash["blocked"],
"blocked_state" => build_hash["blocked_state"],
"message" => build_hash["message"],
"commit" => build_hash["commit"],
"branch" => build_hash["branch"],
"tag" => build_hash["tag"],
"source" => build_hash["source"],
"author" => build_hash["author"],
"created_at" => build_hash["created_at"],
"scheduled_at" => build_hash["scheduled_at"],
"started_at" => build_hash["started_at"],
"finished_at" => build_hash["finished_at"],
"pull_request" => build_hash["pull_request"],
"rebuilt_from" => build_hash["rebuilt_from"],
"meta_data" => build_hash["meta_data"],
}
_build.merge!(author_hash) if build_hash["author"].present?
_build.merge!(creator_hash) if build_hash["creator"].present?
_build
end
private
attr_reader :data
def build_hash
@_build_hash ||= data.fetch("build", {})
end
def author_hash
{
"author" => {
"username" => build_hash["author"]["username"],
"name" => build_hash["author"]["name"],
"email" => build_hash["author"]["email"],
}
}
end
def creator_hash
{
"creator" => {
"id" => build_hash["creator"]["id"],
"name" => build_hash["creator"]["name"],
"email" => build_hash["creator"]["email"],
"avatar_url" => build_hash["creator"]["avatar_url"],
"created_at" => build_hash["creator"]["created_at"],
},
}
end
end
To demo this app, I use the Active Job’s async adapter. For your production use, you should consider Resque or Sidekiq.
That’s it.
You could also implement this in Python, Go, Elixir, and any other programming language of your choice.
Cheers,
Juanito