Начать интеграцию Xcode Bot вручную?
Я смотрел видео WWDC 2014 "Непрерывная интеграция с XCode", и это прекрасно выглядит, как боты могут быть использованы для запуска теста. Но мой вопрос ко всем, кто видел видео, когда он посылает сообщение Дживсу, в котором говорится "интегрировать CoffeeBoard". Бот начинает интегрироваться. Я хочу знать, как он это сделал.
Я хочу добавить хук post-receive на github, который при получении любого коммита должен запускать бот Xcode на моем OS X Server. Большинство членов моей команды используют SourceTree или GitHub для управления своим git, и они не хотят использовать Xcode Source Control. Я думал, что создание бота и настройка его запуска вручную поможет. Мне нужно знать: "Предоставляет ли вам OS X Server такую опцию, как какой-то URL, который запускает бота? "
Извините, если я не достаточно ясно. Но это слишком запутанно для меня, так как у них очень мало документации по триггерам. И хотя он упомянул это как отличную новую функцию, они не включили информацию для достижения этой цели
3 ответа
Предыдущие два ответа не совсем отвечают первоначальному вопросу о том, "как они это сделали", чтобы запустить ботов из приложения "Сообщения".
Я воссоздал точный рабочий процесс и скрипты, необходимые для имитации виртуального помощника Дживса для взаимодействия с ботами (и для получения прогноза погоды).
См. Связанный документ PDF для полных деталей:
https://s3.amazonaws.com/icefield/IntegratingXcodeBotsWithMessages.pdf
Изменить: исходный ответ был удален из-за того, что я ссылался по ссылке на полный ответ. Это изменение добавляет полные детали реализации как часть этого ответа. Я надеюсь, что это не слишком долго для SO ответа.
Интеграция ботов Xcode с сообщениями
На Сессии 415 WWDC 2014, "Непрерывная интеграция с Xcode 6", Apple продемонстрировала интеграцию ботов Xcode с приложением "Сообщения" с помощью пользовательских триггеров интеграции. Более конкретно, начиная с 23-минутной отметки видео этого сеанса ( https://developer.apple.com/videos/play/wwdc2014-415/), Apple демонстрирует использование триггеров интеграции в сочетании с сообщениями для получения статуса Интеграции на сервере сборки. Кроме того, используя виртуального участника комнаты чата Дживса, они демонстрируют возможность запуска интеграций непосредственно из приложения "Сообщения". В следующей статье приведены пошаговые инструкции по воспроизведению этих функций.
Конфигурации клиента и сервера
Чтобы начать, вот конфигурации клиента и сервера, которые я использовал для имитации функциональности Jeeves:
Клиентская версия OS X 10.11 (El Capitan), Xcode 7.0.1
Сервер OS X версии 10.11 (El Capitan), OS X Server 5.0.4, Xcode 7.0.1, Ruby 2.0.0p645
Сеть Для своей разработки и непрерывной интеграции я использую внутреннюю сеть. Мой сервер OS X находится в домене domain.local, а моя машина разработки - это другой узел в той же внутренней сети. Приведенные ниже инструкции должны работать независимо от того, используете ли вы внутренний или внешний сервер.
Jabber - основа сообщений
Jabber - это оригинальное имя протокола с открытым исходным кодом для обмена сообщениями. Jabber был переименован в расширяемый протокол обмена сообщениями и присутствия (XMPP). Приложение OS X Messages создано с использованием Jabber по своей сути.
В этой работе мы будем широко использовать Jabber (Messages), поэтому давайте убедимся, что он включен. В приложении "Сервер OS X" выберите "Службы"> "Сообщения" и включите "Сообщения" в правом верхнем углу. Для Дживса я использовал следующие настройки службы сообщений:
Из окна терминала на вашем сервере, если вы хотите проверить конкретные настройки для Jabber, используйте
$ sudo serveradmin settings jabber
Обратите особое внимание на значения jabberClientPortTLS (5222) и jabberClientPortSSL (5223). Это порты на вашем сервере, которые вы будете использовать для связи со службой Jabber.
Мы будем писать большинство сценариев для Дживса с использованием Ruby, и для этого нам понадобится библиотека XMPP/Jabber. Из окна терминала на вашем сервере установите XMPP4R (библиотека XMPP/Jabber для Ruby), используя
$ gem install xmpp4r
Создать пользователей для сервиса Jabber
Поскольку мой Сервер является локальным сервером без каких-либо учетных записей разработчиков, мне нужно было создать учетные записи для различных разработчиков, чтобы войти в Jabber. Вам может понадобиться или не понадобиться этот шаг в зависимости от того, определены ли на вашем сервере учетные записи пользователей.
От приложения сервера OS X на вашем сервере перейдите к списку Учетные записи> Пользователи и добавьте нового пользователя для каждого клиента, который будет использовать виртуального помощника Дживса. Обязательно создайте нового пользователя для Дживса. Для пользователя "Том", вот настройки, которые были использованы. Обязательно создайте адрес электронной почты для каждого пользователя, но почтовый сервис не должен быть запущен. Эти адреса электронной почты будут использоваться для входа в службу Jabber из приложения Messages на вашем клиенте.
Войдите в Jabber с машины разработки клиентов
С учетными записями пользователей, определенными на вашем сервере, теперь пришло время войти в учетную запись Jabber с вашего клиентского компьютера. В приложении Сообщения на вашем клиенте перейдите в Сообщения> Настройки> Учетные записи. Выберите знак "+" в левом нижнем углу, выберите "Учетная запись других сообщений..." и нажмите "Продолжить". В диалоговом окне Добавить учетную запись сообщений выберите Jabber для Типа учетной записи и введите учетные данные для ваших пользователей. Вот настройки, которые я использовал:
(Обратите внимание, что при включенном SSL порт (5223) соответствует значению jabberClientPortSSL, которое вы указали ранее при проверке настроек службы Jabber на вашем сервере.)
После успешного входа в службу Jabber вы можете при желании изменить псевдоним своей учетной записи на странице "Настройки чата" учетной записи Jabber. Все остальные настройки по умолчанию можно оставить как есть.
Создать чат
Мы хотим, чтобы все статусы интеграции ботов и общения с нашим виртуальным помощником Дживсом проходили через чат. Чаты позволяют групповое общение, но вам не нужно приглашение, чтобы присоединиться. Чтобы создать чат, сделайте следующее.
Из сообщений выберите "Файл"> "Перейти в чат". Вы должны увидеть учетную запись, которую вы вошли в службу Jabber в списке. Введите имя интеграции @ rooms..local для имени комнаты и выберите Go. (Обратите внимание, что я обнаружил, что комната чата должна быть 'rooms..local'.com '>. Использование слова, отличного от' rooms ', не создаст комнату чата.)
Настройка службы веб-сайтов сервера
Когда интеграция запускается из Xcode, работающего на вашем клиентском компьютере, сценарии до и после интеграции связываются со службой Jabber, делая http-вызов файла в службе веб-сайта OS X Server. Вы должны настроить службу веб-сайтов сервера OS X для обработки этих вызовов.
Вам нужно будет изменить настройки для http-сайта без порта SSL (порт 80). Вот настройки, которые я использовал.
Выберите веб-сайт порта 80 и выберите значок карандаша внизу, чтобы ваши настройки соответствовали этим.
Выберите "Изменить дополнительные настройки..." и сделайте так, чтобы ваши настройки соответствовали этим. (Включение "Разрешить выполнение CGI..." включает выполнение сценария Ruby.)
Наконец, вам нужно включить определенный файл (message_room - мы обсудим позже), который будет настроен для работы в качестве сценария Ruby. Для этого поместите следующий файл.htaccess в домашнюю папку вашего веб-сервера по умолчанию (обычно это /Library/Server/Web/Data/Sites/Default).
Options +ExecCGI
<FilesMatch message_room$>
SetHandler cgi-script
</FilesMatch>
ПРИМЕЧАНИЕ: во всех следующих сценариях ruby вам нужно будет изменить переменные под комментарием "учетные данные" в каждом сценарии, чтобы они соответствовали вашему домену и учетным данным для входа.
Сценарии до и после интеграции Когда мы запускаем интеграцию из Xcode на нашем клиентском компьютере, мы хотим отправить сообщение в чат-комнату интеграции Jabber, чтобы все члены чат-комнаты могли получить уведомление о том, что интеграция началась (и закончилась)., Добавьте следующие скрипты до и после интеграции к боту вашего проекта на странице "Триггеры ботов" в XCode.
Это сценарий запуска перед интеграцией:
#!/usr/bin/env ruby
require 'json'
require 'net/http'
require 'uri'
# -------------------------------------------------------------------------------------
# credentials and such
domain = "<yourDomain>.local"
# -------------------------------------------------------------------------------------
# our messaging endpoint
uri = URI.parse("http://#{domain}:80/message_room")
# -------------------------------------------------------------------------------------
# what we want to say
message = "#{ENV['XCS_BOT_NAME']} integration #{ENV['XCS_INTEGRATION_NUMBER']} is now starting."
# -------------------------------------------------------------------------------------
# build up the request body
reqBody = {:message => message}
body = JSON.generate(reqBody)
# -------------------------------------------------------------------------------------
# the connect type
http = Net::HTTP.new(uri.host, uri.port)
# -------------------------------------------------------------------------------------
# build up the request
request = Net::HTTP::Post.new(uri.request_uri)
request.add_field('Content-type', 'application/json')
request.body = body
# -------------------------------------------------------------------------------------
# send the request and get the response
response = http.request(request)
Это триггерный скрипт после интеграции:
#!/usr/bin/env ruby
require 'json'
require 'net/http'
require 'uri'
# -------------------------------------------------------------------------------------
# credentials and such
domain = "<yourDomain>.local"
# -------------------------------------------------------------------------------------
# our messaging endpoint
uri = URI.parse("http://#{domain}:80/message_room")
# -------------------------------------------------------------------------------------
# what we want to say
integrationResult = case ENV['XCS_INTEGRATION_RESULT']
when "succeeded"
"has completed successfully."
when "test-failures"
tc = ENV['XCS_TEST_FAILURE_COUNT'].to_i
"completed with #{tc} failing #{(tc ==1 ) ? 'test' : 'tests'}."
when "build-errors"
ec = ENV['XCS_ERROR_COUNT'].to_i
"failed with #{ec} build #{(ec == 1) ? 'error' : 'errors'}."
when "warnings"
wc = ENV['XCS_WARNING_COUNT'].to_i
"completed with #{wc} #{(wc == 1) ? 'warning' : 'warnings'}."
when "analyzer-warnings"
ic = ENV['XCS_ANALYZER_WARNING_COUNT'].to_i
"completed with #{ic} static analysis #{(ic == 1) ? 'issue' : 'issues'}."
when "trigger-error"
"failed running trigger script."
when "checkout-error"
"failed to checkout from source control."
else
"failed with unexpected errors."
end
message = "#{ENV['XCS_BOT_NAME']} integration #{ENV['XCS_INTEGRATION_NUMBER']} #{integrationResult}"
# -------------------------------------------------------------------------------------
# build up the request body
reqBody = {:message => message}
body = JSON.generate(reqBody)
# -------------------------------------------------------------------------------------
# the connect type
http = Net::HTTP.new(uri.host, uri.port)
# -------------------------------------------------------------------------------------
# build up the request
request = Net::HTTP::Post.new(uri.request_uri)
request.add_field('Content-type', 'application/json')
request.body = body
# -------------------------------------------------------------------------------------
# send the request and get the response
response = http.request(request)
Предыдущие два сценария Ruby выполняют вызов файла message_room, находящегося в домашней папке веб-сайта OS X Server (обычно это /Library/Server/Web/Data/Sites/Default). Поместите следующий файл message_room в эту папку.
#!/usr/bin/env ruby
require 'cgi'
require 'json'
require 'xmpp4r'
require 'xmpp4r/muc'
# -------------------------------------------------------------------------------------
# credentials and such
domain = "<domain>.local"
userId = "jeeves@#{domain}"
userPw = "<jeevesAccountPassword>"
roomName = "integration@rooms.#{domain}"
# -------------------------------------------------------------------------------------
# header sent back
cgi = CGI.new
puts cgi.header( "type" => "text/html", "status" => "OK")
# -------------------------------------------------------------------------------------
# get the message out of the json formatted text
keyValue = JSON.parse(cgi.params.keys.first)
key = "message"
value = keyValue[key] puts value
# -------------------------------------------------------------------------------------
# create the message to the iChat (jabber) room
fromJID = Jabber::JID.new(userId)
jabberClient = Jabber::Client.new(fromJID)
jabberClient.connect
jabberClient.auth(userPw)
jabberClient.send(Jabber::Presence.new.set_type(:available))
# -------------------------------------------------------------------------------------
# send the message to a chat room
roomID = roomName + "/" + jabberClient.jid.node
roomJID = Jabber::JID::new(roomID)
room = Jabber::MUC::MUCClient.new(jabberClient) room.join(roomJID)
roomMessage = Jabber::Message.new(roomJID, value) room.send(roomMessage)
Запуск интеграций из приложения "Сообщения"
Мы хотим иметь возможность давать инструкции нашему виртуальному помощнику Дживсу из приложения Сообщения. Мы собираемся поддержать три инструкции:
Дживс, погода # получает текущую погоду (без почтовых индексов по умолчанию в Купертино)
Дживс, интеграция (имя бота) # запускает интеграцию для данного бота
Дживс, выход # shutdown Дживс на вашем сервере OS X
Следующие файлы будут размещены в папке по умолчанию на веб-сайте вашего сервера OS X (обычно это /Library/Server/Web/Data/Sites/Default).
Основным файлом, который обрабатывает виртуальный помощник, Дживс, является jeevesManager.rb. Запустите этот файл, чтобы разбудить Дживса, введя
$ ruby ./jeevesManager.rb
из папки сайта по умолчанию на вашем сервере.
#!/usr/bin/env ruby
require 'xmpp4r'
require 'xmpp4r/muc'
require 'xmpp4r/delay'
require './jeevesWeather.rb'
require './jeevesIntegration.rb'
# -------------------------------------------------------------------------------------
# credentials and such
domain = "<domain>.local"
userId = "jeeves@#{domain}"
userPw = "<jeevesAccountPassword>"
roomName = "integration@rooms.#{domain}"
defaultWeatherZipCode = "95015"
# -------------------------------------------------------------------------------------
# create the client we'll use
fromJID = Jabber::JID.new(userId)
jabberClient = Jabber::Client.new(fromJID)
jabberClient.connect
jabberClient.auth(userPw)
jabberClient.send(Jabber::Presence.new.set_type(:available))
# -------------------------------------------------------------------------------------
# connect to the chatroom
roomID = roomName + "/" + jabberClient.jid.node
roomJID = Jabber::JID::new(roomID)
room = Jabber::MUC::MUCClient.new(jabberClient) room.join(roomJID)
# -------------------------------------------------------------------------------------
# weather
def getWeather(m)
begin
words = m.body.downcase.split("weather")
where = defaultWeatherZipCode
if (words.length == 2)
where = words[1].strip
end
weather = get_weather_for_city(where,'f')
rescue
weather = "Couldn't get weather for that location - try zip code"
end
return weather
end
# -------------------------------------------------------------------------------------
# integration
def startIntegration(m)
begin
words = m.body.split("integrate")
botName = "Invalid BOT Name"
if (words.length == 2)
botName = words[1].strip
end
integrationMessage = jeevesIntegration(botName)
rescue
integrationMessage = "Failed integrating #{botName}"
end
return integrationMessage
end
# -------------------------------------------------------------------------------------
# listen for messages in chatroom (this callback will run in a separate thread)
room.add_message_callback do |m|
if (m.x.nil?) # the msg is current
if m.type != :error
body = m.body;
if (body.downcase.include? "jeeves")
# assume Jeeves does not understand command
understood = 0
# exit Jeeves
if (body.downcase.include? "exit")
understood = 1
message = "Good-bye"
mainthread.wakeup
end
# Weather
if (body.downcase.include? "weather")
understood = 1
message = getWeather(m)
end
# Integrate BOT
if (body.downcase.include? "integrate")
understood = 1
message = startIntegration(m)
end
# Jeeves doesn't understand command
if (understood == 0)
message = "I don't understand that command!"
end
# let user know what has happened
roomMessage = Jabber::Message.new(roomJID, message)
room.send(roomMessage)
end
end
end
end
# -------------------------------------------------------------------------------------
# add the callback to respond to server ping (to keep the connect alive)
jabberClient.add_iq_callback do |iq_received|
if iq_received.type == :get
if iq_received.queryns.to_s != 'http://jabber.org/protocol/disco#info'
iq = Jabber::Iq.new(:result, jabberClient.jid.node)
iq.id = iq_received.id
iq.from = iq_received.to
iq.to = iq_received.from
jabberClient.send(iq)
end
end
end
# -------------------------------------------------------------------------------------
# stop the main thread (the call back will still be alive this way)
print "Connected to chat room...\n"
Thread.stop
print "Disconnected from chat room...\n"
# leave chat room and log out of Jabber
room.exit
jabberClient.close
Два других дополнительных файла используются файлом менеджера Jeeves выше. Первый ниже описывает получение прогноза погоды и его форматирование, а второй - запуск интеграции.
######### Weather #########
require 'rexml/document'
require 'open-uri'
require 'net/smtp'
# -------------------------------------------------------------------------------------
# yahoo weather url info
# http://developer.yahoo.net/weather/#examples
# -------------------------------------------------------------------------------------
#Returns a hash containing the location and temperature information
#Accepts US zip codes or Yahoo location id's
def yahoo_weather_query(loc_id, units)
h = {}
open("http://xml.weather.yahoo.com/forecastrss?p=#{loc_id}&u=#{units}") do |http|
response = http.read
doc = REXML::Document.new(response)
root = doc.root
channel = root.elements['channel']
location = channel.elements['yweather:location']
h[:city] = location.attributes["city"]
h[:region] = location.attributes["region"]
h[:country] = location.attributes["country"]
h[:temp] = channel.elements["item"].elements["yweather:condition"].attributes["temp"]
h[:text] = channel.elements["item"].elements["yweather:condition"].attributes["text"]
h[:wind_speed] = channel.elements['yweather:wind'].attributes['speed']
h[:humidity] = channel.elements['yweather:atmosphere'].attributes['humidity']
h[:sunrise] = channel.elements['yweather:astronomy'].attributes['sunrise']
h[:sunset] = channel.elements['yweather:astronomy'].attributes['sunset']
h[:forecast_low] = channel.elements["item"].elements['yweather:forecast'].attributes['low']
h[:forecast_high] = channel.elements["item"].elements['yweather:forecast'].attributes['high'] end
return h
end
# -------------------------------------------------------------------------------------
def get_weather_for_city(city_code,units)
weather_info = yahoo_weather_query(city_code, units)
city = weather_info[:city]
region = weather_info[:region]
country = weather_info[:country]
temp = weather_info[:temp]
wind_speed = weather_info[:wind_speed]
humidity = weather_info[:humidity]
text = weather_info[:text]
sunrise = weather_info[:sunrise]
sunset = weather_info[:sunset]
forecast_low = weather_info[:forecast_low]
forecast_high = weather_info[:forecast_high]
return "#{city}, #{region}:\n" + " Currently #{temp} degrees, #{humidity}% humidity, #{wind_speed} mph winds, #{text}.\n" + " Forecast: #{forecast_low} low, #{forecast_high} high.\n" + " Sunrise: #{sunrise}, sunset: #{sunset}.\n"
end
Наконец, это скрипт, который запускает интеграцию из приложения Сообщения
require 'json'
require 'open-uri'
require 'openssl'
# -------------------------------------------------------------------------------------
def jeevesIntegration(botToIntegrate)
# credentials
domain = "<domain>.local"
endpoint = "https://#{domain}:20343"
user = "your-integration-username (not Jeeves)"
password = "password"
# return message
message = "Bot '#{botToIntegrate}' does not exist on server #{domain}"
# request JSON construct with all the BOTS
botsRequestURI = URI.parse("#{endpoint}/api/bots")
output = open(botsRequestURI, {ssl_verify_mode: OpenSSL::SSL::VERIFY_NONE})
bots = JSON.parse(output.readlines.join(""))
# loop through full list of BOTS for the one we're interested in
bots['results'].each do |bot|
botName = bot['name']
if (botName.downcase == botToIntegrate.downcase)
botID = bot['_id']
# curl -k -X POST -u "#{user}:#{password}" "#{endpoint}/api/bots/#{botid}/integrations" -i
# -------------------------------------------------------------------
# kickoff integration
uri = URI.parse(endpoint)
http = Net::HTTP.new(uri.host, uri.port)
http.use_ssl = true
http.verify_mode = OpenSSL::SSL::VERIFY_NONE
request = Net::HTTP::Post.new("/api/bots/#{botID}/integrations")
request.basic_auth(user, password)
response = http.request(request)
message = "Integrating #{botName} on server #{domain}"
end
end
return message
end
Да, как я ответил здесь, сначала нужно выяснить бот _id
а затем отправить POST
запрос к конечной точке бота. Смотрите ссылку для деталей.
Я хочу добавить хук post-receive на github, который при получении любого коммита должен запускать бот Xcode на моем OS X Server.
Если вы хотите "строить на коммите", просто выберите эту опцию при создании бота. У вас есть возможность запускать бот вручную, периодически или по фиксации. Последний делает то, что вы описываете. Как только один из членов вашей команды внесет изменения в репозиторий github, сервер XCode выполнит сборку.