Buildkite Webhook Example

Add an endpoint to your app to handle Webhooks from Buildkite:

post "/buildkite_webhooks" => "buildkite_webhooks#create"

BuildkiteWebhooksController handles the incoming webhook:

# frozen_string_literal: true

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

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!