Add an endpoint to your app to handle Webhooks from Buildkite:
post "/buildkite_webhooks" => "buildkite_webhooks#create"
BuildkiteWebhooksController
handles the incoming webhook, the unsigned Webhook token.
# frozen_string_literal: true
require "openssl"
require "json"
class BuildkiteWebhooksController < ApplicationController
skip_before_action :verify_authenticity_token, only: :create
before_action :verify_buildkite_request
def create
BuildkiteEvent.process(event: buildkite_event, body: body)
head :ok
rescue StandardError => exception
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", "TOKEN").freeze
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
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
Or use this one if youโre using signed Webhook signature:
# frozen_string_literal: true
require "openssl"
require "json"
class SignedBuildkiteWebhooksController < ApplicationController
skip_before_action :verify_authenticity_token, only: :create
before_action :verify_buildkite_request
def create
BuildkiteEvent.process(event: buildkite_event, body: body)
head :ok
rescue StandardError => exception
Rails.logger.error(exception.message)
head :unprocessable_entity
end
private
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
def raw_body
request.body.read
end
# https://buildkite.com/organizations/-/services > Webhook to get the token
BUILDKITE_WEBHOOK_SIGNATURE = ENV.fetch("BUILDKITE_WEBHOOK_SIGNATURE", "SIGNATURE").freeze
class SignedBuildkiteWebhook
def self.valid?(payload, header, secret)
timestamp, signature = get_timestamp_and_signatures(header)
expected = OpenSSL::HMAC.hexdigest("sha256", secret, "#{timestamp}.#{payload}")
Rack::Utils.secure_compare(expected, signature)
end
def self.get_timestamp_and_signatures(header)
parts = header.split(",").map { |kv| kv.split("=", 2).map(&:strip) }.to_h
[parts["timestamp"], parts["signature"]]
end
end
def request_from_buildkite?
SignedBuildkiteWebhook.valid?(raw_body, buildkite_webhook_header, BUILDKITE_WEBHOOK_SIGNATURE)
end
def buildkite_webhook_header
request.headers["X-Buildkite-Signature"].to_s
end
def buildkite_request
request.headers["X-Buildkite-Request"]
end
end
BuildkiteEvent
invokes correct background job based on the webhookโs pipeline is from deploy or other pipelines:
# 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")
if deploy?
BuildkiteDeployNotificationJob.perform_later(payload.build)
else
BuildkiteBuildNotificationJob.perform_later(payload.build)
end
end
private
attr_reader :event, :body
def pipeline_slug
body["pipeline"]["slug"]
end
def deploy?
pipeline_slug == "deploy"
end
def payload
@_payload ||= BuildkitePayload.new(body, pipeline_slug)
end
end
In the background job, you can post any text or message you like based on Build states.
# frozen_string_literal: true
class BuildkiteBuildNotificationJob < ApplicationJob
def perform(build)
number = build["number"]
state = build["state"]
user = user_for(build)
branch = branch_from(build)
slug = build["pipeline"]["slug"]
case state
when "scheduled"
post <<~MESSAGE
๐ฑ [#{slug}] Build ##{number} Scheduled by #{user} (#{branch})
MESSAGE
when "running"
post <<~MESSAGE
โก๏ธ [#{slug}] Build ##{number} Running
MESSAGE
when "passed"
post <<~MESSAGE
๐ [#{slug}] Build ##{number} PASSED
MESSAGE
when "failed"
post <<~MESSAGE
โ [#{slug}] Build ##{number} Failed
View #{build["web_url"]}
MESSAGE
when "canceling"
post <<~MESSAGE
๐ฅ [#{slug}] Build ##{number} Canceling
MESSAGE
when "canceled"
post <<~MESSAGE
๐ [#{slug}] Build ##{number} Canceled
MESSAGE
when "blocked"
post <<~MESSAGE
๐ฆ [#{slug}] Build ##{number} Blocked
MESSAGE
when "skipped"
post <<~MESSAGE
๐ [#{slug}] Build ##{number} Skipped
MESSAGE
when "not_run"
post <<~MESSAGE
๐
๐ปโโ๏ธ [#{slug}] Build ##{number} Not Run
MESSAGE
else
post <<~MESSAGE
๐ช [#{slug}] Build ##{number} #{state}
MESSAGE
end
end
private
ChatID = "Telegram Build Chart ID"
def user_for(build)
build.dig("author", "name").presence ||
build.dig("creator", "name").presence ||
"Scheduler ๐ฆ"
end
def branch_from(build)
build["branch"]
end
def post(message)
client.send_message(chat_id: ChatID, text: message)
end
def client
@_client ||= TelegramClient.new
end
end
BuildkiteDeployNotificationJob
is mostly the same:
# frozen_string_literal: true
class BuildkiteDeployNotificationJob < ApplicationJob
def perform(build)
number = build["number"]
state = build["state"]
user = user_for(build)
case state
when "scheduled"
post <<~MESSAGE
๐ซ Deploy ##{number} Scheduled by #{user}
MESSAGE
when "passed"
post <<~MESSAGE
๐ Deploy ##{number} Deployed successfully
View https://juanitofatas.com/
MESSAGE
when "failed"
post <<~MESSAGE
๐ ##{number} Failed
View #{build["web_url"]}
MESSAGE
when "canceled"
post <<~MESSAGE
๐
๐ปโโ๏ธ ##{number} Canceled
MESSAGE
end
end
private
ChatID = "Telegram Deploy Chat ID"
def user_for(build)
build.dig("author", "name").presence ||
build.dig("creator", "name").presence ||
"Scheduler ๐ฆ"
end
def post(message)
client.send_message(chat_id: ChatID, text: message)
end
def client
@_client ||= TelegramClient.new
end
end
and TelegramClient
uses telegram-bot-ruby under the hook:
# frozen_string_literal: true
require "telegram/bot"
class TelegramClient
Token = ENV.fetch("TELEGRAM_BOT_TOKEN", "ask @botfather to get a token").freeze
def initialize(token: Token)
@token = token
end
def send_message(chat_id:, text:)
api.send_message(chat_id: chat_id, text: text)
end
private
attr_reader :token
def api
@_api ||= Telegram::Bot::Client.new(token).api
end
end
Thatโs it.
Check out the Buildkite Webhooks documentation and Integrations like AWS Lambda, Google Cloud Functions, IronWorker, Zapier, and Webtask. You can also use Amazon EventBridge to stream events to fail fast for your builds :)
Once you deployed a Webhook app, go to your organizationโs notification settings to create a Webhook!