手动启动 Xcode Bot 集成?

2024-02-25

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

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

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


前两个答案并没有完全回答最初的问题“他们是如何做到的”以从消息应用程序中启动机器人。

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

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

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

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

将 Xcode 机器人与消息集成

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

客户端和服务器配置

首先,以下是我用来模仿 Jeeves 功能的客户端和服务器的配置:

ClientOS X 版本 10.11 (El Capitan)、Xcode 7.0.1

ServerOS X 版本 10.11 (El Capitan)、OS X Server 5.0.4、Xcode 7.0.1、Ruby 2.0.0p645

Network为了我的开发和持续集成,我使用内部网络。我的 OS X Server 位于domain.local,我的开发计算机是同一内部网络上的另一个节点。无论您使用的是内部还是外部服务器,以下说明都应该有效。

Jabber – 消息的基础

Jabber 是用于实例消息传递的开源协议的原始名称。 Jabber 更名为可扩展消息传递和状态协议 (XMPP)。 OS X 消息应用程序的核心是使用 Jabber 构建的。

我们将在这项工作中广泛使用 Jabber(消息),所以让我们确保它已打开。从 OS X Server 应用程序中,选择“服务”>“消息”视图,然后打开右上角的“消息”。对于 Jeeves,我使用的消息服务设置如下:

从服务器上的终端窗口中,如果您想检查 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 应用程序,转到帐户 > 用户列表,然后为将使用虚拟 Jeeves 助手的每个客户端添加新用户。请务必为 Jeeves 创建一个新用户。对于用户“Tom”,以下是使用的设置。请务必为每个用户创建一个电子邮件地址,但邮件服务不需要运行。这些电子邮件地址将用于从客户端上的消息应用程序登录 Jabber 服务。

从客户端开发机登录 Jabber

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

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

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

创建聊天室

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

从“消息”中,选择“文件”>“转到聊天室”。您应该会看到列出的登录到 Jabber 服务的帐户。键入[电子邮件受保护] /cdn-cgi/l/email-protection作为房间名称,然后选择前往。 (请注意,我发现聊天室需要是“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​​ 脚本中,您需要修改每个脚本中“credentials”注释下方的变量,以匹配您的域和登录凭据。

集成前和集成后脚本当我们在客户端计算机上从 Xcode 开始集成时,我们希望向 Jabber Integration 聊天室发送一条消息,以便聊天室的所有成员都可以收到集成已开始(和完成)的通知。将以下集成前和集成后脚本添加到 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 脚本调用位于 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)

从消息应用程序启动集成

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

  1. Jeeves, Weather # 获取当前天气(不包含 zip 默认为库比蒂诺)

  2. Jeeves, Integration (Bot Name) # 启动给定的集成 机器人

  3. Jeeves, exit # shutdown OS X 服务器上的 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

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

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
本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

手动启动 Xcode Bot 集成? 的相关文章

随机推荐