mirror of https://github.com/matrix-org/go-neb.git
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
295 lines
9.9 KiB
295 lines
9.9 KiB
// Package travisci implements a Service capable of processing webhooks from Travis-CI.
|
|
package travisci
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"net/http"
|
|
"regexp"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/matrix-org/go-neb/database"
|
|
"github.com/matrix-org/go-neb/types"
|
|
"github.com/matrix-org/gomatrix"
|
|
log "github.com/sirupsen/logrus"
|
|
)
|
|
|
|
// ServiceType of the Travis-CI service.
|
|
const ServiceType = "travis-ci"
|
|
|
|
// DefaultTemplate contains the template that will be used if none is supplied.
|
|
// This matches the default mentioned at: https://docs.travis-ci.com/user/notifications#Customizing-slack-notifications
|
|
const DefaultTemplate = (`%{repository}#%{build_number} (%{branch} - %{commit} : %{author}): %{message}
|
|
Change view : %{compare_url}
|
|
Build details : %{build_url}`)
|
|
|
|
// Matches 'owner/repo'
|
|
var ownerRepoRegex = regexp.MustCompile(`^([A-z0-9-_.]+)/([A-z0-9-_.]+)$`)
|
|
|
|
var httpClient = &http.Client{}
|
|
|
|
// Service contains the Config fields for the Travis-CI service.
|
|
//
|
|
// This service will send notifications into a Matrix room when Travis-CI sends
|
|
// webhook events to it. It requires a public domain which Travis-CI can reach.
|
|
// Notices will be sent as the service user ID.
|
|
//
|
|
// Example JSON request:
|
|
// {
|
|
// rooms: {
|
|
// "!ewfug483gsfe:localhost": {
|
|
// repos: {
|
|
// "matrix-org/go-neb": {
|
|
// template: "%{repository}#%{build_number} (%{branch} - %{commit} : %{author}): %{message}\nBuild details : %{build_url}"
|
|
// }
|
|
// }
|
|
// }
|
|
// }
|
|
// }
|
|
type Service struct {
|
|
types.DefaultService
|
|
webhookEndpointURL string
|
|
// The URL which should be added to .travis.yml - Populated by Go-NEB after Service registration.
|
|
WebhookURL string `json:"webhook_url"`
|
|
// A map from Matrix room ID to Github-style owner/repo repositories.
|
|
Rooms map[string]struct {
|
|
// A map of "owner/repo" to configuration information
|
|
Repos map[string]struct {
|
|
// The template string to use when creating notifications.
|
|
//
|
|
// This is identical to the format of Slack Notifications for Travis-CI:
|
|
// https://docs.travis-ci.com/user/notifications#Customizing-slack-notifications
|
|
//
|
|
// The following variables are available:
|
|
// repository_slug: your GitHub repo identifier (like svenfuchs/minimal)
|
|
// repository_name: the slug without the username
|
|
// build_number: build number
|
|
// build_id: build id
|
|
// branch: branch build name
|
|
// commit: shortened commit SHA
|
|
// author: commit author name
|
|
// commit_message: commit message of build
|
|
// commit_subject: first line of the commit message
|
|
// result: result of build
|
|
// message: Travis CI message to the build
|
|
// duration: total duration of all builds in the matrix
|
|
// elapsed_time: time between build start and finish
|
|
// compare_url: commit change view URL
|
|
// build_url: URL of the build detail
|
|
Template string `json:"template"`
|
|
} `json:"repos"`
|
|
} `json:"rooms"`
|
|
}
|
|
|
|
// The payload from Travis-CI
|
|
type webhookNotification struct {
|
|
ID int `json:"id"`
|
|
Number string `json:"number"`
|
|
Status *int `json:"status"` // 0 (success) or 1 (incomplete/fail).
|
|
StartedAt *string `json:"started_at"`
|
|
FinishedAt *string `json:"finished_at"`
|
|
StatusMessage string `json:"status_message"`
|
|
Commit string `json:"commit"`
|
|
Branch string `json:"branch"`
|
|
Message string `json:"message"`
|
|
CompareURL string `json:"compare_url"`
|
|
CommittedAt string `json:"committed_at"`
|
|
CommitterName string `json:"committer_name"`
|
|
CommitterEmail string `json:"committer_email"`
|
|
AuthorName string `json:"author_name"`
|
|
AuthorEmail string `json:"author_email"`
|
|
Type string `json:"type"`
|
|
BuildURL string `json:"build_url"`
|
|
Repository struct {
|
|
Name string `json:"name"`
|
|
OwnerName string `json:"owner_name"`
|
|
URL string `json:"url"`
|
|
} `json:"repository"`
|
|
}
|
|
|
|
// Converts a webhook notification into a map of template var name to value
|
|
func notifToTemplate(n webhookNotification) map[string]string {
|
|
t := make(map[string]string)
|
|
t["repository_slug"] = n.Repository.OwnerName + "/" + n.Repository.Name
|
|
t["repository"] = t["repository_slug"] // Deprecated form but still used everywhere in people's templates
|
|
t["repository_name"] = n.Repository.Name
|
|
t["build_number"] = n.Number
|
|
t["build_id"] = strconv.Itoa(n.ID)
|
|
t["branch"] = n.Branch
|
|
shaLength := len(n.Commit)
|
|
if shaLength > 10 {
|
|
shaLength = 10
|
|
}
|
|
t["commit"] = n.Commit[:shaLength] // shortened commit SHA
|
|
t["author"] = n.CommitterName // author: commit author name
|
|
// commit_message: commit message of build
|
|
// commit_subject: first line of the commit message
|
|
t["commit_message"] = n.Message
|
|
subjAndMsg := strings.SplitN(n.Message, "\n", 2)
|
|
t["commit_subject"] = subjAndMsg[0]
|
|
if n.Status != nil {
|
|
t["result"] = strconv.Itoa(*n.Status)
|
|
}
|
|
t["message"] = n.StatusMessage // message: Travis CI message to the build
|
|
|
|
if n.StartedAt != nil && n.FinishedAt != nil {
|
|
// duration: total duration of all builds in the matrix -- TODO
|
|
// elapsed_time: time between build start and finish
|
|
// Example from docs: "2011-11-11T11:11:11Z"
|
|
start, err := time.Parse("2006-01-02T15:04:05Z", *n.StartedAt)
|
|
finish, err2 := time.Parse("2006-01-02T15:04:05Z", *n.FinishedAt)
|
|
if err != nil || err2 != nil {
|
|
log.WithFields(log.Fields{
|
|
"started_at": *n.StartedAt,
|
|
"finished_at": *n.FinishedAt,
|
|
}).Warn("Failed to parse Travis-CI start/finish times.")
|
|
} else {
|
|
t["duration"] = finish.Sub(start).String()
|
|
t["elapsed_time"] = t["duration"]
|
|
}
|
|
}
|
|
|
|
t["compare_url"] = n.CompareURL
|
|
t["build_url"] = n.BuildURL
|
|
return t
|
|
}
|
|
|
|
func outputForTemplate(travisTmpl string, tmpl map[string]string) (out string) {
|
|
if travisTmpl == "" {
|
|
travisTmpl = DefaultTemplate
|
|
}
|
|
out = travisTmpl
|
|
for tmplVar, tmplValue := range tmpl {
|
|
out = strings.Replace(out, "%{"+tmplVar+"}", tmplValue, -1)
|
|
}
|
|
return out
|
|
}
|
|
|
|
// OnReceiveWebhook receives requests from Travis-CI and possibly sends requests to Matrix as a result.
|
|
//
|
|
// If the repository matches a known Github repository, a notification will be formed from the
|
|
// template for that repository and a notice will be sent to Matrix.
|
|
//
|
|
// Go-NEB cannot register with Travis-CI for webhooks automatically. The user must manually add the
|
|
// webhook endpoint URL to their .travis.yml file:
|
|
// notifications:
|
|
// webhooks: http://go-neb-endpoint.com/notifications
|
|
//
|
|
// See https://docs.travis-ci.com/user/notifications#Webhook-notifications for more information.
|
|
func (s *Service) OnReceiveWebhook(w http.ResponseWriter, req *http.Request, cli *gomatrix.Client) {
|
|
if err := req.ParseForm(); err != nil {
|
|
log.WithError(err).Error("Failed to read incoming Travis-CI webhook form")
|
|
w.WriteHeader(400)
|
|
return
|
|
}
|
|
payload := req.PostFormValue("payload")
|
|
if payload == "" {
|
|
log.Error("Travis-CI webhook is missing payload= form value")
|
|
w.WriteHeader(400)
|
|
return
|
|
}
|
|
if err := verifyOrigin([]byte(payload), req.Header.Get("Signature")); err != nil {
|
|
log.WithFields(log.Fields{
|
|
"Signature": req.Header.Get("Signature"),
|
|
log.ErrorKey: err,
|
|
}).Warn("Received unauthorised Travis-CI webhook request.")
|
|
w.WriteHeader(403)
|
|
return
|
|
}
|
|
|
|
var notif webhookNotification
|
|
if err := json.Unmarshal([]byte(payload), ¬if); err != nil {
|
|
log.WithError(err).Error("Travis-CI webhook received an invalid JSON payload=")
|
|
w.WriteHeader(400)
|
|
return
|
|
}
|
|
if notif.Repository.OwnerName == "" || notif.Repository.Name == "" {
|
|
log.WithField("repo", notif.Repository).Error("Travis-CI webhook missing repository fields")
|
|
w.WriteHeader(400)
|
|
return
|
|
}
|
|
whForRepo := notif.Repository.OwnerName + "/" + notif.Repository.Name
|
|
tmplData := notifToTemplate(notif)
|
|
|
|
logger := log.WithFields(log.Fields{
|
|
"repo": whForRepo,
|
|
})
|
|
|
|
for roomID, roomData := range s.Rooms {
|
|
for ownerRepo, repoData := range roomData.Repos {
|
|
if ownerRepo != whForRepo {
|
|
continue
|
|
}
|
|
msg := gomatrix.TextMessage{
|
|
Body: outputForTemplate(repoData.Template, tmplData),
|
|
MsgType: "m.notice",
|
|
}
|
|
|
|
logger.WithFields(log.Fields{
|
|
"message": msg,
|
|
"room_id": roomID,
|
|
}).Print("Sending Travis-CI notification to room")
|
|
if _, e := cli.SendMessageEvent(roomID, "m.room.message", msg); e != nil {
|
|
logger.WithError(e).WithField("room_id", roomID).Print(
|
|
"Failed to send Travis-CI notification to room.")
|
|
}
|
|
}
|
|
}
|
|
w.WriteHeader(200)
|
|
}
|
|
|
|
// Register makes sure the Config information supplied is valid.
|
|
func (s *Service) Register(oldService types.Service, client *gomatrix.Client) error {
|
|
s.WebhookURL = s.webhookEndpointURL
|
|
for _, roomData := range s.Rooms {
|
|
for repo := range roomData.Repos {
|
|
match := ownerRepoRegex.FindStringSubmatch(repo)
|
|
if len(match) == 0 {
|
|
return fmt.Errorf("Repository '%s' is not a valid repository name", repo)
|
|
}
|
|
}
|
|
}
|
|
s.joinRooms(client)
|
|
return nil
|
|
}
|
|
|
|
// PostRegister deletes this service if there are no registered repos.
|
|
func (s *Service) PostRegister(oldService types.Service) {
|
|
for _, roomData := range s.Rooms {
|
|
for range roomData.Repos {
|
|
return // at least 1 repo exists
|
|
}
|
|
}
|
|
// Delete this service since no repos are configured
|
|
logger := log.WithFields(log.Fields{
|
|
"service_type": s.ServiceType(),
|
|
"service_id": s.ServiceID(),
|
|
})
|
|
logger.Info("Removing service as no repositories are registered.")
|
|
if err := database.GetServiceDB().DeleteService(s.ServiceID()); err != nil {
|
|
logger.WithError(err).Error("Failed to delete service")
|
|
}
|
|
}
|
|
|
|
func (s *Service) joinRooms(client *gomatrix.Client) {
|
|
for roomID := range s.Rooms {
|
|
if _, err := client.JoinRoom(roomID, "", nil); err != nil {
|
|
log.WithFields(log.Fields{
|
|
log.ErrorKey: err,
|
|
"room_id": roomID,
|
|
"user_id": client.UserID,
|
|
}).Error("Failed to join room")
|
|
}
|
|
}
|
|
}
|
|
|
|
func init() {
|
|
types.RegisterService(func(serviceID, serviceUserID, webhookEndpointURL string) types.Service {
|
|
return &Service{
|
|
DefaultService: types.NewDefaultService(serviceID, serviceUserID, ServiceType),
|
|
webhookEndpointURL: webhookEndpointURL,
|
|
}
|
|
})
|
|
}
|