<pre style='margin:0'>
Zero King (l2dy) pushed a commit to branch master
in repository mpbot-github.

</pre>
<p><a href="https://github.com/macports/mpbot-github/commit/227a913b3c84d4e28a713b7bce7e62a5af6d1565">https://github.com/macports/mpbot-github/commit/227a913b3c84d4e28a713b7bce7e62a5af6d1565</a></p>
<pre style="white-space: pre; background: #F8F8F8"><span style='display:block; white-space:pre;color:#808000;'>commit 227a913b3c84d4e28a713b7bce7e62a5af6d1565
</span>Author: Zero King <l2dy@macports.org>
AuthorDate: Tue Mar 13 10:22:51 2018 +0000

<span style='display:block; white-space:pre;color:#404040;'>    pr: Travis webhook support
</span>---
 ci/logger/constants/constants.go |   3 +
 ci/logger/log.go                 |   5 +-
 pr/prbot/main.go                 |   6 +-
 pr/webhook/server.go             |  94 +++++++++++++++++++--
 pr/webhook/travis.go             | 177 +++++++++++++++++++++++++++++++++++++++
 5 files changed, 274 insertions(+), 11 deletions(-)

<span style='display:block; white-space:pre;color:#808080;'>diff --git a/ci/logger/constants/constants.go b/ci/logger/constants/constants.go
</span>new file mode 100644
<span style='display:block; white-space:pre;color:#808080;'>index 0000000..0818bd2
</span><span style='display:block; white-space:pre;background:#ffe0e0;'>--- /dev/null
</span><span style='display:block; white-space:pre;background:#e0e0ff;'>+++ b/ci/logger/constants/constants.go
</span><span style='display:block; white-space:pre;background:#e0e0e0;'>@@ -0,0 +1,3 @@
</span><span style='display:block; white-space:pre;background:#e0ffe0;'>+package constants
</span><span style='display:block; white-space:pre;background:#e0ffe0;'>+
</span><span style='display:block; white-space:pre;background:#e0ffe0;'>+const MIMEBoundary = "9bba227c544541bbafbe7a4fc806bab5489eae04f16d9303cac42e9eff2e"
</span><span style='display:block; white-space:pre;color:#808080;'>diff --git a/ci/logger/log.go b/ci/logger/log.go
</span><span style='display:block; white-space:pre;color:#808080;'>index e2c9162..35d2400 100644
</span><span style='display:block; white-space:pre;background:#e0e0ff;'>--- a/ci/logger/log.go
</span><span style='display:block; white-space:pre;background:#e0e0ff;'>+++ b/ci/logger/log.go
</span><span style='display:block; white-space:pre;background:#e0e0e0;'>@@ -5,10 +5,12 @@ import (
</span>   "mime/multipart"
        "os"
        "time"
<span style='display:block; white-space:pre;background:#e0ffe0;'>+
</span><span style='display:block; white-space:pre;background:#e0ffe0;'>+   "github.com/macports/mpbot-github/ci/logger/constants"
</span> )
 
 // This is the actual logger used by the CI bot
<span style='display:block; white-space:pre;background:#ffe0e0;'>-var GlobalLogger *Logger = newLogger(os.Stdout)
</span><span style='display:block; white-space:pre;background:#e0ffe0;'>+var GlobalLogger = newLogger(os.Stdout)
</span> 
 func init() {
        go GlobalLogger.Run()
<span style='display:block; white-space:pre;background:#e0e0e0;'>@@ -38,6 +40,7 @@ func newLogger(w io.Writer) *Logger {
</span>           mimeWriter: multipart.NewWriter(w),
                quitChan:   make(chan byte),
        }
<span style='display:block; white-space:pre;background:#e0ffe0;'>+        logger.mimeWriter.SetBoundary(constants.MIMEBoundary)
</span>   logger.remoteLogger = newRemoteLogger(logger)
        return logger
 }
<span style='display:block; white-space:pre;color:#808080;'>diff --git a/pr/prbot/main.go b/pr/prbot/main.go
</span><span style='display:block; white-space:pre;color:#808080;'>index 79be55f..b7206d9 100644
</span><span style='display:block; white-space:pre;background:#e0e0ff;'>--- a/pr/prbot/main.go
</span><span style='display:block; white-space:pre;background:#e0e0ff;'>+++ b/pr/prbot/main.go
</span><span style='display:block; white-space:pre;background:#e0e0e0;'>@@ -33,7 +33,11 @@ func main() {
</span> 
        dbHelper, err := db.NewDBHelper()
        if err != nil {
<span style='display:block; white-space:pre;background:#ffe0e0;'>-                log.Fatal(err)
</span><span style='display:block; white-space:pre;background:#e0ffe0;'>+           if prodFlag {
</span><span style='display:block; white-space:pre;background:#e0ffe0;'>+                   log.Fatal(err)
</span><span style='display:block; white-space:pre;background:#e0ffe0;'>+           } else {
</span><span style='display:block; white-space:pre;background:#e0ffe0;'>+                   log.Println(err)
</span><span style='display:block; white-space:pre;background:#e0ffe0;'>+           }
</span>   }
 
        cronManager := cron.Manager{
<span style='display:block; white-space:pre;color:#808080;'>diff --git a/pr/webhook/server.go b/pr/webhook/server.go
</span><span style='display:block; white-space:pre;color:#808080;'>index cfe73e3..39fa666 100644
</span><span style='display:block; white-space:pre;background:#e0e0ff;'>--- a/pr/webhook/server.go
</span><span style='display:block; white-space:pre;background:#e0e0ff;'>+++ b/pr/webhook/server.go
</span><span style='display:block; white-space:pre;background:#e0e0e0;'>@@ -2,9 +2,15 @@ package webhook
</span> 
 import (
        "context"
<span style='display:block; white-space:pre;background:#e0ffe0;'>+        "crypto"
</span>   "crypto/hmac"
<span style='display:block; white-space:pre;background:#e0ffe0;'>+        "crypto/rsa"
</span>   "crypto/sha1"
<span style='display:block; white-space:pre;background:#e0ffe0;'>+        "crypto/x509"
</span><span style='display:block; white-space:pre;background:#e0ffe0;'>+   "encoding/base64"
</span>   "encoding/hex"
<span style='display:block; white-space:pre;background:#e0ffe0;'>+        "encoding/json"
</span><span style='display:block; white-space:pre;background:#e0ffe0;'>+   "encoding/pem"
</span>   "io/ioutil"
        "log"
        "net/http"
<span style='display:block; white-space:pre;background:#e0e0e0;'>@@ -17,15 +23,17 @@ import (
</span> )
 
 type Receiver struct {
<span style='display:block; white-space:pre;background:#ffe0e0;'>-        server       *http.Server
</span><span style='display:block; white-space:pre;background:#ffe0e0;'>-   hookSecret   []byte
</span><span style='display:block; white-space:pre;background:#ffe0e0;'>-   production   bool
</span><span style='display:block; white-space:pre;background:#ffe0e0;'>-   testing      bool
</span><span style='display:block; white-space:pre;background:#ffe0e0;'>-   githubClient githubapi.Client
</span><span style='display:block; white-space:pre;background:#ffe0e0;'>-   dbHelper     db.DBHelper
</span><span style='display:block; white-space:pre;background:#ffe0e0;'>-   wg           sync.WaitGroup
</span><span style='display:block; white-space:pre;background:#ffe0e0;'>-   members      *map[string]bool
</span><span style='display:block; white-space:pre;background:#ffe0e0;'>-   membersLock  sync.RWMutex
</span><span style='display:block; white-space:pre;background:#e0ffe0;'>+   server           *http.Server
</span><span style='display:block; white-space:pre;background:#e0ffe0;'>+   hookSecret       []byte
</span><span style='display:block; white-space:pre;background:#e0ffe0;'>+   production       bool
</span><span style='display:block; white-space:pre;background:#e0ffe0;'>+   testing          bool
</span><span style='display:block; white-space:pre;background:#e0ffe0;'>+   githubClient     githubapi.Client
</span><span style='display:block; white-space:pre;background:#e0ffe0;'>+   dbHelper         db.DBHelper
</span><span style='display:block; white-space:pre;background:#e0ffe0;'>+   wg               sync.WaitGroup
</span><span style='display:block; white-space:pre;background:#e0ffe0;'>+   members          *map[string]bool
</span><span style='display:block; white-space:pre;background:#e0ffe0;'>+   membersLock      sync.RWMutex
</span><span style='display:block; white-space:pre;background:#e0ffe0;'>+   travisPubKey     *rsa.PublicKey
</span><span style='display:block; white-space:pre;background:#e0ffe0;'>+   travisPubKeyLock sync.RWMutex
</span> }
 
 func NewReceiver(listenAddr string, hookSecret []byte, botSecret string, production bool, dbHelper db.DBHelper) *Receiver {
<span style='display:block; white-space:pre;background:#e0e0e0;'>@@ -88,7 +96,54 @@ func (receiver *Receiver) Start() {
</span>           w.WriteHeader(http.StatusNoContent)
        })
 
<span style='display:block; white-space:pre;background:#e0ffe0;'>+        mux.HandleFunc("/travis", func(w http.ResponseWriter, r *http.Request) {
</span><span style='display:block; white-space:pre;background:#e0ffe0;'>+           sigStr := r.Header.Get("Signature")
</span><span style='display:block; white-space:pre;background:#e0ffe0;'>+
</span><span style='display:block; white-space:pre;background:#e0ffe0;'>+           sig, err := base64.StdEncoding.DecodeString(sigStr)
</span><span style='display:block; white-space:pre;background:#e0ffe0;'>+           if err != nil {
</span><span style='display:block; white-space:pre;background:#e0ffe0;'>+                   log.Println(err)
</span><span style='display:block; white-space:pre;background:#e0ffe0;'>+                   w.WriteHeader(http.StatusBadRequest)
</span><span style='display:block; white-space:pre;background:#e0ffe0;'>+                   return
</span><span style='display:block; white-space:pre;background:#e0ffe0;'>+           }
</span><span style='display:block; white-space:pre;background:#e0ffe0;'>+
</span><span style='display:block; white-space:pre;background:#e0ffe0;'>+           receiver.wg.Add(1)
</span><span style='display:block; white-space:pre;background:#e0ffe0;'>+
</span><span style='display:block; white-space:pre;background:#e0ffe0;'>+           body := []byte(r.FormValue("payload"))
</span><span style='display:block; white-space:pre;background:#e0ffe0;'>+
</span><span style='display:block; white-space:pre;background:#e0ffe0;'>+           if len(body) == 0 {
</span><span style='display:block; white-space:pre;background:#e0ffe0;'>+                   w.WriteHeader(http.StatusBadRequest)
</span><span style='display:block; white-space:pre;background:#e0ffe0;'>+                   receiver.wg.Done()
</span><span style='display:block; white-space:pre;background:#e0ffe0;'>+                   return
</span><span style='display:block; white-space:pre;background:#e0ffe0;'>+           }
</span><span style='display:block; white-space:pre;background:#e0ffe0;'>+
</span><span style='display:block; white-space:pre;background:#e0ffe0;'>+           hashed := sha1.Sum(body)
</span><span style='display:block; white-space:pre;background:#e0ffe0;'>+           receiver.travisPubKeyLock.RLock()
</span><span style='display:block; white-space:pre;background:#e0ffe0;'>+           err = rsa.VerifyPKCS1v15(receiver.travisPubKey, crypto.SHA1, hashed[:], sig)
</span><span style='display:block; white-space:pre;background:#e0ffe0;'>+           receiver.travisPubKeyLock.RUnlock()
</span><span style='display:block; white-space:pre;background:#e0ffe0;'>+           if err != nil {
</span><span style='display:block; white-space:pre;background:#e0ffe0;'>+                   log.Println(err)
</span><span style='display:block; white-space:pre;background:#e0ffe0;'>+                   w.WriteHeader(http.StatusBadRequest)
</span><span style='display:block; white-space:pre;background:#e0ffe0;'>+                   receiver.wg.Done()
</span><span style='display:block; white-space:pre;background:#e0ffe0;'>+                   return
</span><span style='display:block; white-space:pre;background:#e0ffe0;'>+           }
</span><span style='display:block; white-space:pre;background:#e0ffe0;'>+
</span><span style='display:block; white-space:pre;background:#e0ffe0;'>+           var payload TravisWebhookPayload
</span><span style='display:block; white-space:pre;background:#e0ffe0;'>+
</span><span style='display:block; white-space:pre;background:#e0ffe0;'>+           err = json.Unmarshal(body, &payload)
</span><span style='display:block; white-space:pre;background:#e0ffe0;'>+           if err != nil {
</span><span style='display:block; white-space:pre;background:#e0ffe0;'>+                   log.Println(err)
</span><span style='display:block; white-space:pre;background:#e0ffe0;'>+                   w.WriteHeader(http.StatusBadRequest)
</span><span style='display:block; white-space:pre;background:#e0ffe0;'>+                   receiver.wg.Done()
</span><span style='display:block; white-space:pre;background:#e0ffe0;'>+                   return
</span><span style='display:block; white-space:pre;background:#e0ffe0;'>+           }
</span><span style='display:block; white-space:pre;background:#e0ffe0;'>+
</span><span style='display:block; white-space:pre;background:#e0ffe0;'>+           go receiver.handleTravisWebhook(payload)
</span><span style='display:block; white-space:pre;background:#e0ffe0;'>+
</span><span style='display:block; white-space:pre;background:#e0ffe0;'>+           w.WriteHeader(http.StatusNoContent)
</span><span style='display:block; white-space:pre;background:#e0ffe0;'>+   })
</span><span style='display:block; white-space:pre;background:#e0ffe0;'>+
</span>   go receiver.updateMembers()
<span style='display:block; white-space:pre;background:#e0ffe0;'>+        receiver.updateTravisPubKey()
</span> 
        receiver.server.Handler = mux
        receiver.server.ListenAndServe()
<span style='display:block; white-space:pre;background:#e0e0e0;'>@@ -99,6 +154,27 @@ func (receiver *Receiver) Shutdown() {
</span>   receiver.wg.Wait()
 }
 
<span style='display:block; white-space:pre;background:#e0ffe0;'>+func (receiver *Receiver) updateTravisPubKey() {
</span><span style='display:block; white-space:pre;background:#e0ffe0;'>+   const travisPubKeyPEM = "-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAvtjdLkS+FP+0fPC09j25\ny/PiuYDDivIT86COVedvlElk99BBYTrqNaJybxjXbIZ1Q6xFNhOY+iTcBr4E1zJu\ntizF3Xi0V9tOuP/M8Wn4Y/1lCWbQKlWrNQuqNBmhovF4K3mDCYswVbpgTmp+JQYu\nBm9QMdieZMNry5s6aiMA9aSjDlNyedvSENYo18F+NYg1J0C0JiPYTxheCb4optr1\n5xNzFKhAkuGs4XTOA5C7Q06GCKtDNf44s/CVE30KODUxBi0MCKaxiXw/yy55zxX2\n/YdGphIyQiA5iO1986ZmZCLLW8udz9uhW5jUr3Jlp9LbmphAC61bVSf4ou2YsJaN\n0QIDAQAB\n-----END PUBLIC KEY-----"
</span><span style='display:block; white-space:pre;background:#e0ffe0;'>+
</span><span style='display:block; white-space:pre;background:#e0ffe0;'>+   p, _ := pem.Decode([]byte(travisPubKeyPEM))
</span><span style='display:block; white-space:pre;background:#e0ffe0;'>+   if p == nil || p.Type != "PUBLIC KEY" {
</span><span style='display:block; white-space:pre;background:#e0ffe0;'>+           log.Println("travis: invalid public key")
</span><span style='display:block; white-space:pre;background:#e0ffe0;'>+           return
</span><span style='display:block; white-space:pre;background:#e0ffe0;'>+   }
</span><span style='display:block; white-space:pre;background:#e0ffe0;'>+
</span><span style='display:block; white-space:pre;background:#e0ffe0;'>+   travisPubKey, err := x509.ParsePKIXPublicKey(p.Bytes)
</span><span style='display:block; white-space:pre;background:#e0ffe0;'>+   if err != nil {
</span><span style='display:block; white-space:pre;background:#e0ffe0;'>+           return
</span><span style='display:block; white-space:pre;background:#e0ffe0;'>+   }
</span><span style='display:block; white-space:pre;background:#e0ffe0;'>+
</span><span style='display:block; white-space:pre;background:#e0ffe0;'>+   if pubKey, ok := travisPubKey.(*rsa.PublicKey); ok {
</span><span style='display:block; white-space:pre;background:#e0ffe0;'>+           receiver.travisPubKeyLock.Lock()
</span><span style='display:block; white-space:pre;background:#e0ffe0;'>+           receiver.travisPubKey = pubKey
</span><span style='display:block; white-space:pre;background:#e0ffe0;'>+           receiver.travisPubKeyLock.Unlock()
</span><span style='display:block; white-space:pre;background:#e0ffe0;'>+   }
</span><span style='display:block; white-space:pre;background:#e0ffe0;'>+}
</span><span style='display:block; white-space:pre;background:#e0ffe0;'>+
</span> func (receiver *Receiver) updateMembers() {
        for ; ; time.Sleep(24 * time.Hour) {
                users, err := receiver.githubClient.ListOrgMembers("macports")
<span style='display:block; white-space:pre;color:#808080;'>diff --git a/pr/webhook/travis.go b/pr/webhook/travis.go
</span>new file mode 100644
<span style='display:block; white-space:pre;color:#808080;'>index 0000000..865792d
</span><span style='display:block; white-space:pre;background:#ffe0e0;'>--- /dev/null
</span><span style='display:block; white-space:pre;background:#e0e0ff;'>+++ b/pr/webhook/travis.go
</span><span style='display:block; white-space:pre;background:#e0e0e0;'>@@ -0,0 +1,177 @@
</span><span style='display:block; white-space:pre;background:#e0ffe0;'>+package webhook
</span><span style='display:block; white-space:pre;background:#e0ffe0;'>+
</span><span style='display:block; white-space:pre;background:#e0ffe0;'>+import (
</span><span style='display:block; white-space:pre;background:#e0ffe0;'>+   "bufio"
</span><span style='display:block; white-space:pre;background:#e0ffe0;'>+   "io"
</span><span style='display:block; white-space:pre;background:#e0ffe0;'>+   "io/ioutil"
</span><span style='display:block; white-space:pre;background:#e0ffe0;'>+   "log"
</span><span style='display:block; white-space:pre;background:#e0ffe0;'>+   "mime/multipart"
</span><span style='display:block; white-space:pre;background:#e0ffe0;'>+   "net/http"
</span><span style='display:block; white-space:pre;background:#e0ffe0;'>+   "regexp"
</span><span style='display:block; white-space:pre;background:#e0ffe0;'>+   "strconv"
</span><span style='display:block; white-space:pre;background:#e0ffe0;'>+   "strings"
</span><span style='display:block; white-space:pre;background:#e0ffe0;'>+
</span><span style='display:block; white-space:pre;background:#e0ffe0;'>+   "github.com/macports/mpbot-github/ci/logger/constants"
</span><span style='display:block; white-space:pre;background:#e0ffe0;'>+)
</span><span style='display:block; white-space:pre;background:#e0ffe0;'>+
</span><span style='display:block; white-space:pre;background:#e0ffe0;'>+type TravisWebhookPayload struct {
</span><span style='display:block; white-space:pre;background:#e0ffe0;'>+   ID                int    `json:"id"`
</span><span style='display:block; white-space:pre;background:#e0ffe0;'>+   Number            string `json:"number"`
</span><span style='display:block; white-space:pre;background:#e0ffe0;'>+   Type              string `json:"type"`
</span><span style='display:block; white-space:pre;background:#e0ffe0;'>+   State             string `json:"state"`
</span><span style='display:block; white-space:pre;background:#e0ffe0;'>+   Status            int    `json:"status"`
</span><span style='display:block; white-space:pre;background:#e0ffe0;'>+   Result            int    `json:"result"`
</span><span style='display:block; white-space:pre;background:#e0ffe0;'>+   StatusMessage     string `json:"status_message"`
</span><span style='display:block; white-space:pre;background:#e0ffe0;'>+   ResultMessage     string `json:"result_message"`
</span><span style='display:block; white-space:pre;background:#e0ffe0;'>+   Duration          int    `json:"duration"`
</span><span style='display:block; white-space:pre;background:#e0ffe0;'>+   BuildURL          string `json:"build_url"`
</span><span style='display:block; white-space:pre;background:#e0ffe0;'>+   Branch            string `json:"branch"`
</span><span style='display:block; white-space:pre;background:#e0ffe0;'>+   PullRequest       bool   `json:"pull_request"`
</span><span style='display:block; white-space:pre;background:#e0ffe0;'>+   PullRequestNumber int    `json:"pull_request_number"`
</span><span style='display:block; white-space:pre;background:#e0ffe0;'>+   PullRequestTitle  string `json:"pull_request_title"`
</span><span style='display:block; white-space:pre;background:#e0ffe0;'>+   Repository        struct {
</span><span style='display:block; white-space:pre;background:#e0ffe0;'>+           ID        int    `json:"id"`
</span><span style='display:block; white-space:pre;background:#e0ffe0;'>+           Name      string `json:"name"`
</span><span style='display:block; white-space:pre;background:#e0ffe0;'>+           OwnerName string `json:"owner_name"`
</span><span style='display:block; white-space:pre;background:#e0ffe0;'>+   } `json:"repository"`
</span><span style='display:block; white-space:pre;background:#e0ffe0;'>+   Matrix []struct {
</span><span style='display:block; white-space:pre;background:#e0ffe0;'>+           ID       int    `json:"id"`
</span><span style='display:block; white-space:pre;background:#e0ffe0;'>+           ParentID int    `json:"parent_id"`
</span><span style='display:block; white-space:pre;background:#e0ffe0;'>+           Number   string `json:"number"`
</span><span style='display:block; white-space:pre;background:#e0ffe0;'>+           State    string `json:"state"`
</span><span style='display:block; white-space:pre;background:#e0ffe0;'>+           Config   struct {
</span><span style='display:block; white-space:pre;background:#e0ffe0;'>+                   Os       string `json:"os"`
</span><span style='display:block; white-space:pre;background:#e0ffe0;'>+                   OsxImage string `json:"osx_image"`
</span><span style='display:block; white-space:pre;background:#e0ffe0;'>+           } `json:"config"`
</span><span style='display:block; white-space:pre;background:#e0ffe0;'>+           Status       int  `json:"status"`
</span><span style='display:block; white-space:pre;background:#e0ffe0;'>+           Result       int  `json:"result"`
</span><span style='display:block; white-space:pre;background:#e0ffe0;'>+           AllowFailure bool `json:"allow_failure"`
</span><span style='display:block; white-space:pre;background:#e0ffe0;'>+   } `json:"matrix"`
</span><span style='display:block; white-space:pre;background:#e0ffe0;'>+}
</span><span style='display:block; white-space:pre;background:#e0ffe0;'>+
</span><span style='display:block; white-space:pre;background:#e0ffe0;'>+func (receiver *Receiver) handleTravisWebhook(payload TravisWebhookPayload) {
</span><span style='display:block; white-space:pre;background:#e0ffe0;'>+   defer func() {
</span><span style='display:block; white-space:pre;background:#e0ffe0;'>+           if r := recover(); r != nil {
</span><span style='display:block; white-space:pre;background:#e0ffe0;'>+                   log.Println(r)
</span><span style='display:block; white-space:pre;background:#e0ffe0;'>+           }
</span><span style='display:block; white-space:pre;background:#e0ffe0;'>+
</span><span style='display:block; white-space:pre;background:#e0ffe0;'>+           if !receiver.testing {
</span><span style='display:block; white-space:pre;background:#e0ffe0;'>+                   receiver.wg.Done()
</span><span style='display:block; white-space:pre;background:#e0ffe0;'>+           }
</span><span style='display:block; white-space:pre;background:#e0ffe0;'>+   }()
</span><span style='display:block; white-space:pre;background:#e0ffe0;'>+
</span><span style='display:block; white-space:pre;background:#e0ffe0;'>+   if !payload.PullRequest {
</span><span style='display:block; white-space:pre;background:#e0ffe0;'>+           return
</span><span style='display:block; white-space:pre;background:#e0ffe0;'>+   }
</span><span style='display:block; white-space:pre;background:#e0ffe0;'>+
</span><span style='display:block; white-space:pre;background:#e0ffe0;'>+   if payload.Repository.OwnerName != "macports" && payload.Repository.OwnerName != "macports-staging" {
</span><span style='display:block; white-space:pre;background:#e0ffe0;'>+           return
</span><span style='display:block; white-space:pre;background:#e0ffe0;'>+   }
</span><span style='display:block; white-space:pre;background:#e0ffe0;'>+
</span><span style='display:block; white-space:pre;background:#e0ffe0;'>+   log.Println("PR #" + strconv.Itoa(payload.PullRequestNumber) + " " + payload.ResultMessage + " on Travis CI")
</span><span style='display:block; white-space:pre;background:#e0ffe0;'>+
</span><span style='display:block; white-space:pre;background:#e0ffe0;'>+   comment := "[Travis Build #" + payload.Number + "](" + payload.BuildURL + ") " + payload.ResultMessage + ".\n\n"
</span><span style='display:block; white-space:pre;background:#e0ffe0;'>+   timeOut := false
</span><span style='display:block; white-space:pre;background:#e0ffe0;'>+   lintDone := false
</span><span style='display:block; white-space:pre;background:#e0ffe0;'>+
</span><span style='display:block; white-space:pre;background:#e0ffe0;'>+   log.Println("Processing " + strconv.Itoa(len(payload.Matrix)) + " job(s)")
</span><span style='display:block; white-space:pre;background:#e0ffe0;'>+
</span><span style='display:block; white-space:pre;background:#e0ffe0;'>+   for _, job := range payload.Matrix {
</span><span style='display:block; white-space:pre;background:#e0ffe0;'>+           req, err := http.NewRequest(
</span><span style='display:block; white-space:pre;background:#e0ffe0;'>+                   "GET",
</span><span style='display:block; white-space:pre;background:#e0ffe0;'>+                   "https://api.travis-ci.org/job/"+strconv.Itoa(job.ID)+"/log",
</span><span style='display:block; white-space:pre;background:#e0ffe0;'>+                   nil,
</span><span style='display:block; white-space:pre;background:#e0ffe0;'>+           )
</span><span style='display:block; white-space:pre;background:#e0ffe0;'>+           if err != nil {
</span><span style='display:block; white-space:pre;background:#e0ffe0;'>+                   continue
</span><span style='display:block; white-space:pre;background:#e0ffe0;'>+           }
</span><span style='display:block; white-space:pre;background:#e0ffe0;'>+
</span><span style='display:block; white-space:pre;background:#e0ffe0;'>+           req.Header.Set("Travis-API-Version", "3")
</span><span style='display:block; white-space:pre;background:#e0ffe0;'>+           req.Header.Set("Accept", "text/plain")
</span><span style='display:block; white-space:pre;background:#e0ffe0;'>+
</span><span style='display:block; white-space:pre;background:#e0ffe0;'>+           log.Println("Fetching logs for job #" + strconv.Itoa(job.ID))
</span><span style='display:block; white-space:pre;background:#e0ffe0;'>+
</span><span style='display:block; white-space:pre;background:#e0ffe0;'>+           resp, err := http.DefaultClient.Do(req)
</span><span style='display:block; white-space:pre;background:#e0ffe0;'>+           if err != nil {
</span><span style='display:block; white-space:pre;background:#e0ffe0;'>+                   continue
</span><span style='display:block; white-space:pre;background:#e0ffe0;'>+           }
</span><span style='display:block; white-space:pre;background:#e0ffe0;'>+
</span><span style='display:block; white-space:pre;background:#e0ffe0;'>+           bufReader := bufio.NewReader(resp.Body)
</span><span style='display:block; white-space:pre;background:#e0ffe0;'>+
</span><span style='display:block; white-space:pre;background:#e0ffe0;'>+           for {
</span><span style='display:block; white-space:pre;background:#e0ffe0;'>+                   line, err := bufReader.ReadString('\n')
</span><span style='display:block; white-space:pre;background:#e0ffe0;'>+                   if err != nil {
</span><span style='display:block; white-space:pre;background:#e0ffe0;'>+                           break
</span><span style='display:block; white-space:pre;background:#e0ffe0;'>+                   }
</span><span style='display:block; white-space:pre;background:#e0ffe0;'>+                   if strings.Contains(line, "$ sudo ./runner") {
</span><span style='display:block; white-space:pre;background:#e0ffe0;'>+                           break
</span><span style='display:block; white-space:pre;background:#e0ffe0;'>+                   }
</span><span style='display:block; white-space:pre;background:#e0ffe0;'>+           }
</span><span style='display:block; white-space:pre;background:#e0ffe0;'>+
</span><span style='display:block; white-space:pre;background:#e0ffe0;'>+           body, err := ioutil.ReadAll(bufReader)
</span><span style='display:block; white-space:pre;background:#e0ffe0;'>+           resp.Body.Close()
</span><span style='display:block; white-space:pre;background:#e0ffe0;'>+           if err != nil {
</span><span style='display:block; white-space:pre;background:#e0ffe0;'>+                   continue
</span><span style='display:block; white-space:pre;background:#e0ffe0;'>+           }
</span><span style='display:block; white-space:pre;background:#e0ffe0;'>+           bodyStr := string(body)
</span><span style='display:block; white-space:pre;background:#e0ffe0;'>+           bodyStr = strings.Replace(bodyStr, "\n\n\n\r", "\n", -1)
</span><span style='display:block; white-space:pre;background:#e0ffe0;'>+           bodyStr = strings.Replace(bodyStr, "\r", "", -1)
</span><span style='display:block; white-space:pre;background:#e0ffe0;'>+
</span><span style='display:block; white-space:pre;background:#e0ffe0;'>+           mr := multipart.NewReader(strings.NewReader(bodyStr), constants.MIMEBoundary)
</span><span style='display:block; white-space:pre;background:#e0ffe0;'>+           for {
</span><span style='display:block; white-space:pre;background:#e0ffe0;'>+                   p, err := mr.NextPart()
</span><span style='display:block; white-space:pre;background:#e0ffe0;'>+                   if err == io.EOF {
</span><span style='display:block; white-space:pre;background:#e0ffe0;'>+                           break
</span><span style='display:block; white-space:pre;background:#e0ffe0;'>+                   }
</span><span style='display:block; white-space:pre;background:#e0ffe0;'>+                   if err.Error() == "unexpected EOF" {
</span><span style='display:block; white-space:pre;background:#e0ffe0;'>+                           if strings.Contains(
</span><span style='display:block; white-space:pre;background:#e0ffe0;'>+                                   bodyStr,
</span><span style='display:block; white-space:pre;background:#e0ffe0;'>+                                   "The job exceeded the maximum time limit for jobs, and has been terminated.",
</span><span style='display:block; white-space:pre;background:#e0ffe0;'>+                           ) {
</span><span style='display:block; white-space:pre;background:#e0ffe0;'>+                                   timeOut = true
</span><span style='display:block; white-space:pre;background:#e0ffe0;'>+                           }
</span><span style='display:block; white-space:pre;background:#e0ffe0;'>+                           continue
</span><span style='display:block; white-space:pre;background:#e0ffe0;'>+                   }
</span><span style='display:block; white-space:pre;background:#e0ffe0;'>+                   if err != nil {
</span><span style='display:block; white-space:pre;background:#e0ffe0;'>+                           log.Println(err)
</span><span style='display:block; white-space:pre;background:#e0ffe0;'>+                           return
</span><span style='display:block; white-space:pre;background:#e0ffe0;'>+                   }
</span><span style='display:block; white-space:pre;background:#e0ffe0;'>+                   pName := p.FormName()
</span><span style='display:block; white-space:pre;background:#e0ffe0;'>+                   content, err := ioutil.ReadAll(p)
</span><span style='display:block; white-space:pre;background:#e0ffe0;'>+                   if pName == "keep-alive" {
</span><span style='display:block; white-space:pre;background:#e0ffe0;'>+                           continue
</span><span style='display:block; white-space:pre;background:#e0ffe0;'>+                   }
</span><span style='display:block; white-space:pre;background:#e0ffe0;'>+                   if err != nil {
</span><span style='display:block; white-space:pre;background:#e0ffe0;'>+                           log.Println(err)
</span><span style='display:block; white-space:pre;background:#e0ffe0;'>+                           continue
</span><span style='display:block; white-space:pre;background:#e0ffe0;'>+                   }
</span><span style='display:block; white-space:pre;background:#e0ffe0;'>+                   if strings.HasPrefix(pName, "port-lint-output-") && len(content) > 0 && !lintDone {
</span><span style='display:block; white-space:pre;background:#e0ffe0;'>+                           comment += "<details><summary>Lint results</summary>\n\n```\n" + string(content) + "```\n</details>\n\n<br>\n\n"
</span><span style='display:block; white-space:pre;background:#e0ffe0;'>+                           lintDone = true
</span><span style='display:block; white-space:pre;background:#e0ffe0;'>+                   }
</span><span style='display:block; white-space:pre;background:#e0ffe0;'>+                   if strings.HasSuffix(pName, "-pastebin") {
</span><span style='display:block; white-space:pre;background:#e0ffe0;'>+                           pastebinRegex := regexp.MustCompile(`^port-(.*)(-dep)?-install-output-(success|fail)-pastebin$`)
</span><span style='display:block; white-space:pre;background:#e0ffe0;'>+                           pbInfo := pastebinRegex.FindStringSubmatch(pName)
</span><span style='display:block; white-space:pre;background:#e0ffe0;'>+                           if pbInfo == nil {
</span><span style='display:block; white-space:pre;background:#e0ffe0;'>+                                   continue
</span><span style='display:block; white-space:pre;background:#e0ffe0;'>+                           }
</span><span style='display:block; white-space:pre;background:#e0ffe0;'>+                           comment += "Port " + pbInfo[1]
</span><span style='display:block; white-space:pre;background:#e0ffe0;'>+                           if pbInfo[2] == "-dep" {
</span><span style='display:block; white-space:pre;background:#e0ffe0;'>+                                   comment += "'s dependencies"
</span><span style='display:block; white-space:pre;background:#e0ffe0;'>+                           }
</span><span style='display:block; white-space:pre;background:#e0ffe0;'>+                           comment += " **" + pbInfo[3] + "** on " + job.Config.OsxImage + ". [Log](" + string(content) + ")\n"
</span><span style='display:block; white-space:pre;background:#e0ffe0;'>+                   }
</span><span style='display:block; white-space:pre;background:#e0ffe0;'>+           }
</span><span style='display:block; white-space:pre;background:#e0ffe0;'>+   }
</span><span style='display:block; white-space:pre;background:#e0ffe0;'>+
</span><span style='display:block; white-space:pre;background:#e0ffe0;'>+   if timeOut {
</span><span style='display:block; white-space:pre;background:#e0ffe0;'>+           comment += "The build timed out."
</span><span style='display:block; white-space:pre;background:#e0ffe0;'>+   }
</span><span style='display:block; white-space:pre;background:#e0ffe0;'>+
</span><span style='display:block; white-space:pre;background:#e0ffe0;'>+   receiver.githubClient.CreateComment(
</span><span style='display:block; white-space:pre;background:#e0ffe0;'>+           payload.Repository.OwnerName,
</span><span style='display:block; white-space:pre;background:#e0ffe0;'>+           payload.Repository.Name,
</span><span style='display:block; white-space:pre;background:#e0ffe0;'>+           payload.PullRequestNumber,
</span><span style='display:block; white-space:pre;background:#e0ffe0;'>+           &comment,
</span><span style='display:block; white-space:pre;background:#e0ffe0;'>+   )
</span><span style='display:block; white-space:pre;background:#e0ffe0;'>+}
</span></pre><pre style='margin:0'>

</pre>