5

我正在观看 WWDC 2014“与 Xcode 的持续集成”视频,它看起来很棒如何使用机器人来运行测试。但我的问题是任何看过视频的人,当他向 Jeeves 发送消息说“集成 CoffeeBoard”时,机器人开始集成。我想知道他是怎么做到的。

我想在 github 上添加 post-receive 钩子,它在收到任何提交时都应该在我的 OS X 服务器上启动 Xcode bot。我的大多数团队成员使用 SourceTree 或 GitHub 来管理他们的 git,他们不想使用 Xcode Source Control。我认为创建一个机器人并将其选项设置为手动启动就可以了。我需要知道,“OS X Server 是否为您提供了某种 url 之类的选项来启动机器人?”

对不起,如果我不够清楚。但这对我来说太令人困惑了,因为他们关于触发器的文档非常少。尽管他提到它是很酷的新功能,但他们没有提供任何信息来实现这一目标

4

3 回答 3

9

前两个答案并没有完全回答“他们如何做到这一点”以从 Messages 应用程序中启动机器人的原始问题。

我重新创建了模仿 Jeeves 虚拟助手与机器人交互(以及获取天气)所需的确切工作流程和脚本。

有关完整详细信息,请参阅链接的 PDF 文档:

https://s3.amazonaws.com/icefield/IntegratingXcodeBotsWithMessages.pdf

编辑:我相信,原始答案已被删除,因为我通过指向完整答案的链接引用了这一事实。此编辑添加了完整的实现细节作为此答案的一部分。我希望这样的答案不会太长。

将 Xcode 机器人与消息集成

在 WWDC 2014 Session 415,与 Xcode 6 的持续集成期间,Apple 展示了通过自定义集成触发器将 Xcode 机器人与 Messages 应用程序集成。更具体地说,从该会话视频的第 23 分钟开始 ( https://developer.apple.com/videos/play/wwdc2014-415/),Apple演示了将集成触发器与 Messages 结合使用来接收构建服务器上的集成。此外,通过使用虚拟聊天室成员 Jeeves,他们展示了直接从 Messages 应用程序中开始集成的能力。以下文章提供了重现该功能的分步说明。

客户端和服务器配置

首先,以下是我用来模仿 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(消息),所以让我们确保它处于打开状态。在 OS X Server 应用程序中,选择服务 > 消息视图,然后打开右上角的消息。对于 Jeeves,我使用的 Messages 服务设置如下:

消息服务设置

从服务器上的终端窗口中,如果要检查 Jabber 的特定设置,请使用

$ sudo serveradmin settings jabber

请特别注意 jabberClientPortTLS (5222) 和 jabberClientPortSSL (5223) 值。这些是服务器上用于与 Jabber 服务通信的端口。

我们将使用 Ruby 为 Jeeves 编写大部分脚本,并且我们需要一个 XMPP/Jabber 库来完成这项工作。从服务器上的终端窗口,安装 XMPP4R(Ruby 的 XMPP/Jabber 库)使用

$ gem install xmpp4r

为 Jabber 服务创建用户

因为我的服务器是一个本地服务器,上面没有任何开发人员帐户,所以我需要为各种开发人员创建帐户以登录 Jabber。您可能需要也可能不需要此步骤,具体取决于您的服务器是否已定义用户帐户。

从您服务器上的 OS X Server 应用程序中,转到 Accounts > Users 列表,并为将使用虚拟 Jeeves 助手的每个客户端添加新用户。请务必为 Jeeves 创建一个新用户。对于用户“Tom”,这里是使用的设置。确保为每个用户创建一个电子邮件地址,但邮件服务不需要运行。这些电子邮件地址将用于从您客户端上的消息应用程序登录 Jabber 服务。

用户设置

从客户端开发机器登录到 Jabber

在您的服务器上定义了用户帐户后,现在是时候从您的客户端计算机登录到 Jabber 帐户了。在客户端上的消息应用程序中,转到消息 > 首选项 > 帐户。选择左下角的 + 号,选择“其他消息帐户...”,然后按继续。在添加消息帐户对话框中,为帐户类型选择 Jabber,然后为您的用户填写凭据信息。这是我使用的设置:

在此处输入图像描述

(注意启用 SSL,端口 (5223) 与您之前在检查服务器上的 Jabber 服务设置时列出的 jabberClientPortSSL 值匹配。)

成功登录 Jabber 服务后,您可以选择在 Jabber 帐户的“聊天设置”页面下更改您的帐户昵称。所有其他默认设置都可以保持原样。

创建聊天室

我们希望所有机器人集成状态和与我们的虚拟助手 Jeeves 的通信都通过 Messages 聊天室。聊天室允许群组交流,但您不需要邀请即可加入。要创建聊天室,请执行以下操作。

从消息中,选择文件 > 转到聊天室。您应该会看到您登录 Jabber 服务的帐户已列出。键入 integration@rooms..local 作为房间名称,然后选择 Go。(请注意,我发现聊天室必须是“rooms..local”.com'>。使用“rooms”以外的词不会创建聊天室。)

创建聊天室

配置服务器网站服务

当从客户端计算机上运行的 Xcode 启动集成时,集成前和集成后脚本通过对 OS X Server 网站服务上的文件进行 http 调用与 Jabber 服务通信。您必须配置 OS X Server 网站服务来处理这些调用。

您需要修改非 SSL http(端口 80)站点的设置。这是我使用的设置。

网络服务器设置

选择端口 80 网站,然后选择下面的铅笔图标以使您的设置与这些匹配。

网络服务器设置

选择“编辑高级设置...”并使您的设置与这些匹配。(启用“允许 CGI 执行...”启用 Ruby 脚本执行。)

网络服务器设置

最后,您需要启用特定文件(message_room - 我们将在稍后讨论)配置为作为 Ruby 脚本运行。为此,请将以下 .htaccess 文件放在 Web 服务器的默认主文件夹中(通常是 /Library/Server/Web/Data/Sites/Default)。

Options +ExecCGI 
<FilesMatch message_room$>
    SetHandler cgi-script 
</FilesMatch>

注意:在以下所有 ruby​​ 脚本中,您需要修改每个脚本中“凭据”注释下的变量以匹配您的域和登录凭据。

集成前和集成后脚本 当我们在客户端机器上从 Xcode 开始集成时,我们希望向 Jabber 集成聊天室发送一条消息,以便通知聊天室的所有成员集成已经开始(和完成) . 在 Xcode 中的 bot Triggers 页面上,将以下集成前和集成后脚本添加到项目的 bot。

这是预集成触发器脚本:

#!/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 脚本调用了位于 OS X Server 网站主文件夹(通常是 /Library/Server/Web/Data/Sites/Default)中的 message_room 文件。将以下 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)

从消息应用程序开始集成

我们希望能够从 Messages 应用程序中向我们的虚拟助手 Jeeves 发出指令。我们将支持三个指令:

  1. Jeeves, weather # 获取当前天气(无 zip 默认为 Cupertino)

  2. Jeeves, integration (Bot Name) # 启动给定 Bot 的集成

  3. Jeeves,在您的 OS X 服务器上退出 #shutdown Jeeves

以下文件将放置在您的 OS X Server 网站的默认文件夹中(通常是 /Library/Server/Web/Data/Sites/Default)。

处理虚拟助手 Jeeves 的主文件是 jeevesManager.rb。通过输入启动此文件以唤醒 Jeeves

$ 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

最后,这是从 Messages 应用程序启动集成的脚本

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
于 2015-10-29T21:26:25.953 回答
2

是的,正如我在这里回答的那样,您首先需要找出机器人_id,然后向POST机器人的端点发送请求。有关详细信息,请参阅链接。

于 2015-05-05T20:23:27.960 回答
1

I want to add post-receive hook on github which on receiving any commit should start Xcode bot on my OS X Server.

If you want to 'build on commit' then just select that option when you create the bot. You have the option to run the bot Manually, Periodically or On Commit. The latter does what you describe. As soon as one of your team members commits a change to your github repo, Xcode server will do a build.

于 2015-03-04T15:47:22.267 回答