【问题标题】:Start Integration of Xcode Bot manually?手动开始集成 Xcode Bot?
【发布时间】:2015-04-06 13:19:32
【问题描述】:

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

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

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

【问题讨论】:

    标签: xcode xcodebuild xcode-bots


    【解决方案1】:

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

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

    完整详情请参阅链接的 PDF 文档:

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

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

    将 Xcode 机器人与消息集成

    在 WWDC 2014 Session 415,与 Xcode 6 的持续集成期间,Apple 演示了通过自定义集成触发器将 Xcode 机器人与 Messages 应用程序集成。更具体地说,从该会话视频 (https://developer.apple.com/videos/play/wwdc2014-415/) 的第 23 分钟开始,Apple 演示了将集成触发器与消息结合使用来接收构建服务器上的集成状态。此外,通过使用虚拟聊天室成员 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 应用程序中,转到“帐户”>“用户”列表,并为将使用虚拟 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 Websites 服务来处理这些调用。

    您需要修改非 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 Integration 聊天室发送消息,以便通知聊天室的所有成员集成已经开始(和完成)。在 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 # 获取当前天气(w/o zip 默认为库比蒂诺)

    2. Jeeves, integration (Bot Name) # 开始给定的集成 机器人

    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
    

    【讨论】:

      【解决方案2】:

      是的,作为我answered here,您首先需要找出机器人_id,然后向机器人的端点发送POST 请求。详情见链接。

      【讨论】:

        【解决方案3】:

        我想在 github 上添加 post-receive 钩子,它在收到任何提交时都应该在我的 OS X 服务器上启动 Xcode bot。

        如果您想“在提交时构建”,则只需在创建机器人时选择该选项。您可以选择手动定期提交时运行机器人。后者按照您的描述进行。只要您的团队成员之一对您的 github 存储库提交更改,Xcode 服务器就会进行构建。

        【讨论】:

        • 请注意,'On Commit' 不会在提交时启动,它每 5 分钟检查一次是否已推送提交,然后执行机器人。要在提交时获得实际集成,您需要像 GitHub 上这样的钩子,并让机器人从钩子中的脚本手动运行。
        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2018-03-27
        • 2018-11-03
        • 2016-12-08
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多