github.com/marinho/drone@v0.2.1-0.20140504195434-d3ba962e89a7/pkg/handler/github.go (about)

     1  package handler
     2  
     3  import (
     4  	"database/sql"
     5  	"net/http"
     6  	"strconv"
     7  	"time"
     8  
     9  	"github.com/drone/drone/pkg/build/script"
    10  	"github.com/drone/drone/pkg/database"
    11  	. "github.com/drone/drone/pkg/model"
    12  	"github.com/drone/drone/pkg/queue"
    13  	"github.com/drone/go-github/github"
    14  )
    15  
    16  type GithubHandler struct {
    17  	queue *queue.Queue
    18  }
    19  
    20  func NewGithubHandler(queue *queue.Queue) *GithubHandler {
    21  	return &GithubHandler{
    22  		queue: queue,
    23  	}
    24  }
    25  
    26  // Processes a generic POST-RECEIVE GitHub hook and
    27  // attempts to trigger a build.
    28  func (h *GithubHandler) Hook(w http.ResponseWriter, r *http.Request) error {
    29  	// handle github ping
    30  	if r.Header.Get("X-Github-Event") == "ping" {
    31  		return RenderText(w, http.StatusText(http.StatusOK), http.StatusOK)
    32  	}
    33  
    34  	// if this is a pull request route
    35  	// to a different handler
    36  	if r.Header.Get("X-Github-Event") == "pull_request" {
    37  		h.PullRequestHook(w, r)
    38  		return nil
    39  	}
    40  
    41  	// get the payload of the message
    42  	// this should contain a json representation of the
    43  	// repository and commit details
    44  	payload := r.FormValue("payload")
    45  
    46  	// parse the github Hook payload
    47  	hook, err := github.ParseHook([]byte(payload))
    48  	if err != nil {
    49  		println("could not parse hook:", err.Error())
    50  		return RenderText(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest)
    51  	}
    52  
    53  	// make sure this is being triggered because of a commit
    54  	// and not something like a tag deletion or whatever
    55  	if hook.IsTag() || hook.IsGithubPages() ||
    56  		hook.IsHead() == false || hook.IsDeleted() {
    57  		return RenderText(w, http.StatusText(http.StatusOK), http.StatusOK)
    58  	}
    59  
    60  	// get the repo from the URL
    61  	repoId := r.FormValue("id")
    62  
    63  	// get the repo from the database, return error if not found
    64  	repo, err := database.GetRepoSlug(repoId)
    65  	if err != nil {
    66  		return RenderText(w, http.StatusText(http.StatusNotFound), http.StatusNotFound)
    67  	}
    68  
    69  	// Get the user that owns the repository
    70  	user, err := database.GetUser(repo.UserID)
    71  	if err != nil {
    72  		return RenderText(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest)
    73  	}
    74  
    75  	// Verify that the commit doesn't already exist.
    76  	// We should never build the same commit twice.
    77  	_, err = database.GetCommitBranchHash(hook.Branch(), hook.Head.Id, repo.ID)
    78  	if err != nil && err != sql.ErrNoRows {
    79  		println("commit already exists")
    80  		return RenderText(w, http.StatusText(http.StatusBadGateway), http.StatusBadGateway)
    81  	}
    82  
    83  	// we really only need:
    84  	//  * repo owner
    85  	//  * repo name
    86  	//  * repo host (github)
    87  	//  * commit hash
    88  	//  * commit timestamp
    89  	//  * commit branch
    90  	//  * commit message
    91  	//  * commit author
    92  	//  * pull request
    93  
    94  	// once we have this data we could just send directly to the queue
    95  	// and let it handle everything else
    96  
    97  	commit := &Commit{}
    98  	commit.RepoID = repo.ID
    99  	commit.Branch = hook.Branch()
   100  	commit.Hash = hook.Head.Id
   101  	commit.Status = "Pending"
   102  	commit.Created = time.Now().UTC()
   103  
   104  	// extract the author and message from the commit
   105  	// this is kind of experimental, since I don't know
   106  	// what I'm doing here.
   107  	if hook.Head != nil && hook.Head.Author != nil {
   108  		commit.Message = hook.Head.Message
   109  		commit.Timestamp = hook.Head.Timestamp
   110  		commit.SetAuthor(hook.Head.Author.Email)
   111  	} else if hook.Commits != nil && len(hook.Commits) > 0 && hook.Commits[0].Author != nil {
   112  		commit.Message = hook.Commits[0].Message
   113  		commit.Timestamp = hook.Commits[0].Timestamp
   114  		commit.SetAuthor(hook.Commits[0].Author.Email)
   115  	}
   116  
   117  	// get the github settings from the database
   118  	settings := database.SettingsMust()
   119  
   120  	// get the drone.yml file from GitHub
   121  	client := github.New(user.GithubToken)
   122  	client.ApiUrl = settings.GitHubApiUrl
   123  
   124  	content, err := client.Contents.FindRef(repo.Owner, repo.Name, ".drone.yml", commit.Hash)
   125  	if err != nil {
   126  		msg := "No .drone.yml was found in this repository.  You need to add one.\n"
   127  		if err := saveFailedBuild(commit, msg); err != nil {
   128  			return RenderText(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
   129  		}
   130  		return RenderText(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest)
   131  	}
   132  
   133  	// decode the content.  Note: Not sure this will ever happen...it basically means a GitHub API issue
   134  	raw, err := content.DecodeContent()
   135  	if err != nil {
   136  		msg := "Could not decode the yaml from GitHub.  Check that your .drone.yml is a valid yaml file.\n"
   137  		if err := saveFailedBuild(commit, msg); err != nil {
   138  			return RenderText(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
   139  		}
   140  		return RenderText(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest)
   141  	}
   142  
   143  	// parse the build script
   144  	buildscript, err := script.ParseBuild(raw, repo.Params)
   145  	if err != nil {
   146  		msg := "Could not parse your .drone.yml file.  It needs to be a valid drone yaml file.\n\n" + err.Error() + "\n"
   147  		if err := saveFailedBuild(commit, msg); err != nil {
   148  			return RenderText(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
   149  		}
   150  		return RenderText(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest)
   151  	}
   152  
   153  	// save the commit to the database
   154  	if err := database.SaveCommit(commit); err != nil {
   155  		return RenderText(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
   156  	}
   157  
   158  	// save the build to the database
   159  	build := &Build{}
   160  	build.Slug = "1" // TODO
   161  	build.CommitID = commit.ID
   162  	build.Created = time.Now().UTC()
   163  	build.Status = "Pending"
   164  	if err := database.SaveBuild(build); err != nil {
   165  		return RenderText(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
   166  	}
   167  
   168  	// notify websocket that a new build is pending
   169  	//realtime.CommitPending(repo.UserID, repo.TeamID, repo.ID, commit.ID, repo.Private)
   170  	//realtime.BuildPending(repo.UserID, repo.TeamID, repo.ID, commit.ID, build.ID, repo.Private)
   171  
   172  	h.queue.Add(&queue.BuildTask{Repo: repo, Commit: commit, Build: build, Script: buildscript}) //Push(repo, commit, build, buildscript)
   173  
   174  	// OK!
   175  	return RenderText(w, http.StatusText(http.StatusOK), http.StatusOK)
   176  }
   177  
   178  func (h *GithubHandler) PullRequestHook(w http.ResponseWriter, r *http.Request) {
   179  	// get the payload of the message
   180  	// this should contain a json representation of the
   181  	// repository and commit details
   182  	payload := r.FormValue("payload")
   183  
   184  	println("GOT PR HOOK")
   185  	println(payload)
   186  
   187  	hook, err := github.ParsePullRequestHook([]byte(payload))
   188  	if err != nil {
   189  		RenderText(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest)
   190  		return
   191  	}
   192  
   193  	// ignore these
   194  	if hook.Action != "opened" && hook.Action != "synchronize" {
   195  		RenderText(w, http.StatusText(http.StatusOK), http.StatusOK)
   196  		return
   197  	}
   198  
   199  	// get the repo from the URL
   200  	repoId := r.FormValue("id")
   201  
   202  	// get the repo from the database, return error if not found
   203  	repo, err := database.GetRepoSlug(repoId)
   204  	if err != nil {
   205  		RenderText(w, http.StatusText(http.StatusNotFound), http.StatusNotFound)
   206  		return
   207  	}
   208  
   209  	// Get the user that owns the repository
   210  	user, err := database.GetUser(repo.UserID)
   211  	if err != nil {
   212  		RenderText(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest)
   213  		return
   214  	}
   215  
   216  	// Verify that the commit doesn't already exist.
   217  	// We should enver build the same commit twice.
   218  	_, err = database.GetCommitHash(hook.PullRequest.Head.Sha, repo.ID)
   219  	if err != nil && err != sql.ErrNoRows {
   220  		RenderText(w, http.StatusText(http.StatusBadGateway), http.StatusBadGateway)
   221  		return
   222  	}
   223  
   224  	///////////////////////////////////////////////////////
   225  
   226  	commit := &Commit{}
   227  	commit.RepoID = repo.ID
   228  	commit.Branch = hook.PullRequest.Head.Ref
   229  	commit.Hash = hook.PullRequest.Head.Sha
   230  	commit.Status = "Pending"
   231  	commit.Created = time.Now().UTC()
   232  	commit.Gravatar = hook.PullRequest.User.GravatarId
   233  	commit.Author = hook.PullRequest.User.Login
   234  	commit.PullRequest = strconv.Itoa(hook.Number)
   235  	commit.Message = hook.PullRequest.Title
   236  	// label := p.PullRequest.Head.Labe
   237  
   238  	// get the github settings from the database
   239  	settings := database.SettingsMust()
   240  
   241  	// get the drone.yml file from GitHub
   242  	client := github.New(user.GithubToken)
   243  	client.ApiUrl = settings.GitHubApiUrl
   244  
   245  	content, err := client.Contents.FindRef(repo.Owner, repo.Name, ".drone.yml", commit.Hash) // TODO should this really be the hash??
   246  	if err != nil {
   247  		println(err.Error())
   248  		RenderText(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest)
   249  		return
   250  	}
   251  
   252  	// decode the content
   253  	raw, err := content.DecodeContent()
   254  	if err != nil {
   255  		RenderText(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest)
   256  		return
   257  	}
   258  
   259  	// parse the build script
   260  	buildscript, err := script.ParseBuild(raw, repo.Params)
   261  	if err != nil {
   262  		// TODO if the YAML is invalid we should create a commit record
   263  		// with an ERROR status so that the user knows why a build wasn't
   264  		// triggered in the system
   265  		RenderText(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest)
   266  		return
   267  	}
   268  
   269  	// save the commit to the database
   270  	if err := database.SaveCommit(commit); err != nil {
   271  		RenderText(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
   272  		return
   273  	}
   274  
   275  	// save the build to the database
   276  	build := &Build{}
   277  	build.Slug = "1" // TODO
   278  	build.CommitID = commit.ID
   279  	build.Created = time.Now().UTC()
   280  	build.Status = "Pending"
   281  	if err := database.SaveBuild(build); err != nil {
   282  		RenderText(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
   283  		return
   284  	}
   285  
   286  	// notify websocket that a new build is pending
   287  	// TODO we should, for consistency, just put this inside Queue.Add()
   288  	h.queue.Add(&queue.BuildTask{Repo: repo, Commit: commit, Build: build, Script: buildscript})
   289  
   290  	// OK!
   291  	RenderText(w, http.StatusText(http.StatusOK), http.StatusOK)
   292  }