手动开始Xcode Bot的集成?

Tas*_*een 5 xcode xcodebuild xcode-bots

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

我想在github上添加post-receive hook,在接收任何提交时应该在我的OS X Server上启动Xcode bot.我的大多数团队成员使用SourceTree或GitHub来管理他们的git,他们不想使用Xcode Source Control.我认为创建一个机器人并设置其手动启动的选项将成功.我需要知道,"OS X Server是否会为您提供类似于启动机器人的URL的选项?"

对不起,如果我不够清楚的话.但这对我来说太混乱了,因为他们对触发器的文档很少.虽然他提到它是很酷的新功能,但他们没有包含任何信息来实现这一目标

ice*_*eld 9

The prior two answers don't exactly answer the original question of "how they did that" to kick off bots from within Messages app.

I've recreated the exact workflow and scripts required to mimic the Jeeves virtual assistant for interacting with bots (and for getting the weather).

See the linked PDF document for the complete details:

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

Edit: original answer was deleted due, I believe, to the fact that I referenced via a link to the full answer. This edit adds the full implementation details as part of this answer. I hope it is not too long for a SO answer.

Integrating Xcode Bots with Messages

During WWDC 2014 Session 415, Continuous Integration with Xcode 6, Apple demonstrated integrating Xcode bots with the Messages app via custom integration triggers. More specifically, starting at the 23 minute mark of that session’s video (https://developer.apple.com/videos/play/wwdc2014-415/), Apple demonstrates the use of integration triggers in conjunction with Messages to receive the status of integrations on a build server. Furthermore, through the use of a virtual chat room member, Jeeves, they demonstrate the ability to start integrations directly from within the Messages app. The following article provides step-by-step instructions to reproduce that functionality.

Client and Server Configurations

To get started, here are the configurations of the client and server I used to mimic the Jeeves functionality:

Client OS X Version 10.11 (El Capitan), Xcode 7.0.1

Server OS X Version 10.11 (El Capitan), OS X Server 5.0.4, Xcode 7.0.1, Ruby 2.0.0p645

Network For my development and continuous integrations, I use an internal network. My OS X Server is at domain.local, and my development machine is another node on the same internal network. The instructions below should work regardless if you’re using an internal or external server.

Jabber – The foundation of Messages

Jabber is the original name of an open source protocol for instance messaging. Jabber was renamed the Extensible Messaging and Presence Protocol (XMPP). The OS X Messages app is built using Jabber at its core.

We’ll use Jabber (Messages) extensively in this effort, so let’s ensure it is on. From the OS X Server App, select the Services > Messages view, and toggle on Messages in the upper right corner. For Jeeves, the Messages service settings I used are as follows:

消息服务设置

From a terminal window on your server, if you want to check the specific settings for Jabber, use

$ sudo serveradmin settings jabber
Run Code Online (Sandbox Code Playgroud)

Note in particular the jabberClientPortTLS (5222) and jabberClientPortSSL (5223) values. These are the ports on your server that you will use to communicate with the Jabber service.

We’ll be writing most of the scripts for Jeeves using Ruby, and we’ll need a XMPP/Jabber library to accomplish this. From a terminal window on your server, install XMPP4R (an XMPP/Jabber library for Ruby) using

$ gem install xmpp4r
Run Code Online (Sandbox Code Playgroud)

Create Users for Jabber Service

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

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

用户设置

从Client Development Machine登录Jabber

With the user account(s) defined on your server, it’s now time to login to the Jabber account from your client machine. In the Messages app on your client, go to Messages > Preferences > Accounts. Select the + sign in the lower left, select "Other Messages Account..." and press Continue. In the Add a Messages account dialog, select Jabber for the Account Type, and fill in the credential information for your user(s). Here were the settings I used:

在此输入图像描述

(Note with SSL toggled on, the Port (5223) matches the jabberClientPortSSL value you listed earlier when checking the settings of the Jabber service on your server.)

After successfully logging in to the Jabber service, you can optionally change your account nickname under the "Chat Settings" page of the Jabber account. All other default settings are okay to leave as is.

Create Chat Room

我们希望所有僵尸程序集成状态和与我们的虚拟助手Jeeves的通信都通过消息聊天室.聊天室允许群组通信,但您不需要加入邀请.要创建聊天室,请执行以下操作.

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

创建聊天室

配置服务器网站服务

When an integration is started from Xcode running on your client machine, the pre- and post- integration scripts communicate with the Jabber service by making an http call to a file on the OS X Server Website Service. You must configure the OS X Server Websites service to handle these calls.

You’ll need to modify the settings for the non-SSL http (port 80) site. Here are the settings I used.

Web服务器设置

Select the Port 80 Website, and select the pencil icon underneath to make your settings match these.

Web服务器设置

Select the "Edit Advanced Settings..." and make your settings match these. (Enabling "Allow CGI execution..." enables Ruby script execution.)

Web服务器设置

Finally, you’ll need to enable a particular file (message_room – we’ll discuss later) to be configured to run as a Ruby script. To do that, place the following .htaccess file in your web server’s default home folder (typically /Library/Server/Web/Data/Sites/Default).

Options +ExecCGI 
<FilesMatch message_room$>
    SetHandler cgi-script 
</FilesMatch>
Run Code Online (Sandbox Code Playgroud)

NOTE: in all of the following ruby scripts, you’ll need to modify the variables just under the "credentials" comment in each script to match your domain, and login credentials.

Pre- and Post-Integration Scripts When we start an integration from Xcode on our client machine, we want to send a message to the Jabber Integration chat room so that all members of the chat room can be notified the integration has started (and finished). Add the following pre- and post-integration scripts to your project’s bot on the bot Triggers page within Xcode.

This is the pre-integration Trigger script:

#!/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)
Run Code Online (Sandbox Code Playgroud)

This is the post-integration Trigger script:

#!/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)
Run Code Online (Sandbox Code Playgroud)

The prior two Ruby scripts make a call to the message_room file residing in your OS X Server Website home folder (typically /Library/Server/Web/Data/Sites/Default). Place the following message_room file into that folder.

#!/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)
Run Code Online (Sandbox Code Playgroud)

Starting Integrations from the Messages App

We want to be able to issue instructions to our virtual assistant Jeeves from within the Messages App. We’re going to support three instructions:

  1. Jeeves, weather # gets the current weather (w/o zip defaults to Cupertino)

  2. Jeeves, integration (Bot Name) # starts an integration for the given Bot

  3. Jeeves, exit # shutdown Jeeves on your OS X Server

The following files will be placed in your OS X Server website’s default folder (typically /Library/Server/Web/Data/Sites/Default).

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

$ ruby ./jeevesManager.rb
Run Code Online (Sandbox Code Playgroud)

从网站上的服务器默认文件夹.

#!/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
Run Code Online (Sandbox Code Playgroud)

上面的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
Run Code Online (Sandbox Code Playgroud)

最后,这是启动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
Run Code Online (Sandbox Code Playgroud)