368 lines
11 KiB
Go
368 lines
11 KiB
Go
// Copyright 2019 Drone IO, Inc.
|
|
//
|
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
// you may not use this file except in compliance with the License.
|
|
// You may obtain a copy of the License at
|
|
//
|
|
// http://www.apache.org/licenses/LICENSE-2.0
|
|
//
|
|
// Unless required by applicable law or agreed to in writing, software
|
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
// See the License for the specific language governing permissions and
|
|
// limitations under the License.
|
|
|
|
package parser
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
"net/http"
|
|
"net/http/httputil"
|
|
"os"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/drone/drone/core"
|
|
"github.com/drone/go-scm/scm"
|
|
)
|
|
|
|
// TODO(bradrydzewski): stash, push hook missing link
|
|
// TODO(bradrydzewski): stash, tag hook missing timestamp
|
|
// TODO(bradrydzewski): stash, tag hook missing commit message
|
|
// TODO(bradrydzewski): stash, tag hook missing link
|
|
// TODO(bradrydzewski): stash, pull request hook missing link
|
|
// TODO(bradrydzewski): stash, hooks missing repository clone http url
|
|
// TODO(bradrydzewski): stash, hooks missing repository clone ssh url
|
|
// TODO(bradrydzewski): stash, hooks missing repository html link
|
|
|
|
// TODO(bradrydzewski): gogs, push hook missing author avatar, using sender instead.
|
|
// TODO(bradrydzewski): gogs, pull request hook missing commit sha.
|
|
// TODO(bradrydzewski): gogs, tag hook missing commit sha.
|
|
// TODO(bradrydzewski): gogs, sender missing Name field.
|
|
// TODO(bradrydzewski): gogs, push hook missing repository html url
|
|
|
|
// TODO(bradrydzewski): gitea, push hook missing author avatar, using sender instead.
|
|
// TODO(bradrydzewski): gitea, tag hook missing commit sha.
|
|
// TODO(bradrydzewski): gitea, sender missing Name field.
|
|
// TODO(bradrydzewski): gitea, push hook missing repository html url
|
|
|
|
// TODO(bradrydzewski): bitbucket, pull request hook missing author email.
|
|
// TODO(bradrydzewski): bitbucket, hooks missing default repository branch.
|
|
|
|
// TODO(bradrydzewski): github, push hook timestamp is negative value.
|
|
// TODO(bradrydzewski): github, pull request message is empty
|
|
|
|
// represents a deleted ref in the github webhook.
|
|
const emptyCommit = "0000000000000000000000000000000000000000"
|
|
|
|
// this is intended for local testing and instructs the handler
|
|
// to print the contents of the hook to stdout.
|
|
var debugPrintHook = false
|
|
|
|
func init() {
|
|
debugPrintHook, _ = strconv.ParseBool(
|
|
os.Getenv("DRONE_DEBUG_DUMP_HOOK"),
|
|
)
|
|
}
|
|
|
|
// New returns a new HookParser.
|
|
func New(client *scm.Client) core.HookParser {
|
|
return &parser{client}
|
|
}
|
|
|
|
type parser struct {
|
|
client *scm.Client
|
|
}
|
|
|
|
func (p *parser) Parse(req *http.Request, secretFunc func(string) string) (*core.Hook, *core.Repository, error) {
|
|
if debugPrintHook {
|
|
// if DRONE_DEBUG_DUMP_HOOK=true print the http.Request
|
|
// headers and body to stdout.
|
|
out, _ := httputil.DumpRequest(req, true)
|
|
os.Stderr.Write(out)
|
|
}
|
|
|
|
// callback function provides the webhook parser with
|
|
// a per-repository secret key used to verify the webhook
|
|
// payload signature for authenticity.
|
|
fn := func(webhook scm.Webhook) (string, error) {
|
|
if webhook == nil {
|
|
return "", errors.New("Invalid or malformed webhook")
|
|
}
|
|
repo := webhook.Repository()
|
|
slug := scm.Join(repo.Namespace, repo.Name)
|
|
secret := secretFunc(slug)
|
|
if secret == "" {
|
|
return secret, errors.New("Cannot find repository")
|
|
}
|
|
return secret, nil
|
|
}
|
|
|
|
payload, err := p.client.Webhooks.Parse(req, fn)
|
|
if err == scm.ErrUnknownEvent {
|
|
return nil, nil, nil
|
|
}
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
|
|
var repo *core.Repository
|
|
var hook *core.Hook
|
|
|
|
switch v := payload.(type) {
|
|
case *scm.PushHook:
|
|
// github sends push hooks when tags and branches are
|
|
// deleted. These hooks should be ignored.
|
|
if v.Commit.Sha == emptyCommit {
|
|
return nil, nil, nil
|
|
}
|
|
// github sends push hooks when tags are created. The
|
|
// push hook contains more information than the tag hook,
|
|
// so we choose to use the push hook for tags.
|
|
if strings.HasPrefix(v.Ref, "refs/tags/") {
|
|
hook = &core.Hook{
|
|
Trigger: core.TriggerHook, // core.TriggerHook
|
|
Event: core.EventTag,
|
|
Action: core.ActionCreate,
|
|
Link: v.Commit.Link,
|
|
Timestamp: v.Commit.Author.Date.Unix(),
|
|
Message: v.Commit.Message,
|
|
Before: v.Before,
|
|
After: v.Commit.Sha,
|
|
Source: scm.TrimRef(v.BaseRef),
|
|
Target: scm.TrimRef(v.BaseRef),
|
|
Ref: v.Ref,
|
|
Author: v.Commit.Author.Login,
|
|
AuthorName: v.Commit.Author.Name,
|
|
AuthorEmail: v.Commit.Author.Email,
|
|
AuthorAvatar: v.Commit.Author.Avatar,
|
|
Sender: v.Sender.Login,
|
|
}
|
|
} else {
|
|
hook = &core.Hook{
|
|
Trigger: core.TriggerHook, //core.TriggerHook,
|
|
Event: core.EventPush,
|
|
Link: v.Commit.Link,
|
|
Timestamp: v.Commit.Author.Date.Unix(),
|
|
Message: v.Commit.Message,
|
|
Before: v.Before,
|
|
After: v.Commit.Sha,
|
|
Ref: v.Ref,
|
|
Source: strings.TrimPrefix(v.Ref, "refs/heads/"),
|
|
Target: strings.TrimPrefix(v.Ref, "refs/heads/"),
|
|
Author: v.Commit.Author.Login,
|
|
AuthorName: v.Commit.Author.Name,
|
|
AuthorEmail: v.Commit.Author.Email,
|
|
AuthorAvatar: v.Commit.Author.Avatar,
|
|
Sender: v.Sender.Login,
|
|
}
|
|
}
|
|
repo = &core.Repository{
|
|
UID: v.Repo.ID,
|
|
Namespace: v.Repo.Namespace,
|
|
Name: v.Repo.Name,
|
|
Slug: scm.Join(v.Repo.Namespace, v.Repo.Name),
|
|
Link: v.Repo.Link,
|
|
Branch: v.Repo.Branch,
|
|
Private: v.Repo.Private,
|
|
HTTPURL: v.Repo.Clone,
|
|
SSHURL: v.Repo.CloneSSH,
|
|
}
|
|
// gogs and gitea do not include the author avatar in
|
|
// the webhook, but they do include the sender avatar.
|
|
// use the sender avatar when necessary.
|
|
if hook.AuthorAvatar == "" {
|
|
hook.AuthorAvatar = v.Sender.Avatar
|
|
}
|
|
return hook, repo, nil
|
|
case *scm.TagHook:
|
|
if v.Action != scm.ActionCreate {
|
|
return nil, nil, nil
|
|
}
|
|
// when a tag is created github sends both a push hook
|
|
// and a tag create hook. The push hook contains more
|
|
// information, so we choose to use the push hook and
|
|
// ignore the native tag hook.
|
|
if p.client.Driver == scm.DriverGithub ||
|
|
p.client.Driver == scm.DriverGitea ||
|
|
p.client.Driver == scm.DriverGitlab {
|
|
return nil, nil, nil
|
|
}
|
|
|
|
// the tag hook does not include the commit link, message
|
|
// or timestamp. In some cases it does not event include
|
|
// the sha (gogs). Note that we may need to fetch additional
|
|
// details to augment the webhook.
|
|
hook = &core.Hook{
|
|
Trigger: core.TriggerHook, // core.TriggerHook,
|
|
Event: core.EventTag,
|
|
Action: core.ActionCreate,
|
|
Link: "",
|
|
Timestamp: 0,
|
|
Message: "",
|
|
After: v.Ref.Sha,
|
|
Ref: v.Ref.Name,
|
|
Source: v.Ref.Name,
|
|
Target: v.Ref.Name,
|
|
Author: v.Sender.Login,
|
|
AuthorName: v.Sender.Name,
|
|
AuthorEmail: v.Sender.Email,
|
|
AuthorAvatar: v.Sender.Avatar,
|
|
Sender: v.Sender.Login,
|
|
}
|
|
repo = &core.Repository{
|
|
UID: v.Repo.ID,
|
|
Namespace: v.Repo.Namespace,
|
|
Name: v.Repo.Name,
|
|
Slug: scm.Join(v.Repo.Namespace, v.Repo.Name),
|
|
Link: v.Repo.Link,
|
|
Branch: v.Repo.Branch,
|
|
Private: v.Repo.Private,
|
|
HTTPURL: v.Repo.Clone,
|
|
SSHURL: v.Repo.CloneSSH,
|
|
}
|
|
// TODO(bradrydzewski) can we use scm.ExpandRef here?
|
|
if !strings.HasPrefix(hook.Ref, "refs/tags/") {
|
|
hook.Ref = fmt.Sprintf("refs/tags/%s", hook.Ref)
|
|
}
|
|
if hook.AuthorAvatar == "" {
|
|
hook.AuthorAvatar = v.Sender.Avatar
|
|
}
|
|
return hook, repo, nil
|
|
case *scm.PullRequestHook:
|
|
if v.Action != scm.ActionOpen && v.Action != scm.ActionSync {
|
|
return nil, nil, nil
|
|
}
|
|
// Pull Requests are not supported for Bitbucket due
|
|
// to lack of refs (e.g. refs/pull-requests/42/from).
|
|
// Please contact Bitbucket Support if you would like to
|
|
// see this feature enabled:
|
|
// https://bitbucket.org/site/master/issues/5814/repository-refs-for-pull-requests
|
|
if p.client.Driver == scm.DriverBitbucket {
|
|
return nil, nil, nil
|
|
}
|
|
hook = &core.Hook{
|
|
Trigger: core.TriggerHook, // core.TriggerHook,
|
|
Event: core.EventPullRequest,
|
|
Action: v.Action.String(),
|
|
Link: v.PullRequest.Link,
|
|
Timestamp: v.PullRequest.Created.Unix(),
|
|
Title: v.PullRequest.Title,
|
|
Message: v.PullRequest.Body,
|
|
Before: v.PullRequest.Base.Sha,
|
|
After: v.PullRequest.Sha,
|
|
Ref: v.PullRequest.Ref,
|
|
Fork: v.PullRequest.Fork,
|
|
Source: v.PullRequest.Source,
|
|
Target: v.PullRequest.Target,
|
|
Author: v.PullRequest.Author.Login,
|
|
AuthorName: v.PullRequest.Author.Name,
|
|
AuthorEmail: v.PullRequest.Author.Email,
|
|
AuthorAvatar: v.PullRequest.Author.Avatar,
|
|
Sender: v.Sender.Login,
|
|
}
|
|
// HACK this is a workaround for github. The pull
|
|
// request title is populated, but not the message.
|
|
if hook.Message == "" {
|
|
hook.Message = hook.Title
|
|
}
|
|
repo = &core.Repository{
|
|
UID: v.Repo.ID,
|
|
Namespace: v.Repo.Namespace,
|
|
Name: v.Repo.Name,
|
|
Slug: scm.Join(v.Repo.Namespace, v.Repo.Name),
|
|
Link: v.Repo.Link,
|
|
Branch: v.Repo.Branch,
|
|
Private: v.Repo.Private,
|
|
HTTPURL: v.Repo.Clone,
|
|
SSHURL: v.Repo.CloneSSH,
|
|
}
|
|
if hook.AuthorAvatar == "" {
|
|
hook.AuthorAvatar = v.Sender.Avatar
|
|
}
|
|
return hook, repo, nil
|
|
case *scm.BranchHook:
|
|
if v.Action != scm.ActionCreate {
|
|
return nil, nil, nil
|
|
}
|
|
if p.client.Driver != scm.DriverStash {
|
|
return nil, nil, nil
|
|
}
|
|
hook = &core.Hook{
|
|
Trigger: core.TriggerHook, // core.TriggerHook,
|
|
Event: core.EventPush,
|
|
Link: "",
|
|
Timestamp: 0,
|
|
Message: "",
|
|
After: v.Ref.Sha,
|
|
Ref: v.Ref.Name,
|
|
Source: v.Ref.Name,
|
|
Target: v.Ref.Name,
|
|
Author: v.Sender.Login,
|
|
AuthorName: v.Sender.Name,
|
|
AuthorEmail: v.Sender.Email,
|
|
AuthorAvatar: v.Sender.Avatar,
|
|
Sender: v.Sender.Login,
|
|
}
|
|
repo = &core.Repository{
|
|
UID: v.Repo.ID,
|
|
Namespace: v.Repo.Namespace,
|
|
Name: v.Repo.Name,
|
|
Slug: scm.Join(v.Repo.Namespace, v.Repo.Name),
|
|
Link: v.Repo.Link,
|
|
Branch: v.Repo.Branch,
|
|
Private: v.Repo.Private,
|
|
HTTPURL: v.Repo.Clone,
|
|
SSHURL: v.Repo.CloneSSH,
|
|
}
|
|
return hook, repo, nil
|
|
case *scm.DeployHook:
|
|
hook = &core.Hook{
|
|
Trigger: core.TriggerHook,
|
|
Event: core.EventPromote,
|
|
Link: v.TargetURL,
|
|
Timestamp: time.Now().Unix(),
|
|
Message: v.Desc,
|
|
After: v.Ref.Sha,
|
|
Ref: v.Ref.Path,
|
|
Source: v.Ref.Name,
|
|
Target: v.Ref.Name,
|
|
Author: v.Sender.Login,
|
|
AuthorName: v.Sender.Name,
|
|
AuthorEmail: v.Sender.Email,
|
|
AuthorAvatar: v.Sender.Avatar,
|
|
Sender: v.Sender.Login,
|
|
Deployment: v.Target,
|
|
DeploymentID: v.Number,
|
|
Params: toMap(v.Data),
|
|
}
|
|
repo = &core.Repository{
|
|
UID: v.Repo.ID,
|
|
Namespace: v.Repo.Namespace,
|
|
Name: v.Repo.Name,
|
|
Slug: scm.Join(v.Repo.Namespace, v.Repo.Name),
|
|
Link: v.Repo.Link,
|
|
Branch: v.Repo.Branch,
|
|
Private: v.Repo.Private,
|
|
HTTPURL: v.Repo.Clone,
|
|
SSHURL: v.Repo.CloneSSH,
|
|
}
|
|
return hook, repo, nil
|
|
default:
|
|
return nil, nil, nil
|
|
}
|
|
}
|
|
|
|
func toMap(src interface{}) map[string]string {
|
|
set, ok := src.(map[string]interface{})
|
|
if !ok {
|
|
return nil
|
|
}
|
|
dst := map[string]string{}
|
|
for k, v := range set {
|
|
dst[k] = fmt.Sprint(v)
|
|
}
|
|
return nil
|
|
}
|