// ParseHook parses the post-commit hook from the Request body // and returns the required data in a standard format. func (r *GitHub) ParseHook(req *http.Request) (*model.Hook, error) { // handle github ping if req.Header.Get("X-Github-Event") == "ping" { return nil, nil } // handle github pull request hook differently if req.Header.Get("X-Github-Event") == "pull_request" { return r.ParsePullRequestHook(req) } // parse the github Hook payload var payload = GetPayload(req) var data, err = github.ParseHook(payload) if err != nil { return nil, nil } // make sure this is being triggered because of a commit // and not something like a tag deletion or whatever if data.IsTag() || data.IsGithubPages() || data.IsHead() == false || data.IsDeleted() { return nil, nil } var hook = new(model.Hook) hook.Repo = data.Repo.Name hook.Owner = data.Repo.Owner.Login hook.Sha = data.Head.Id hook.Branch = data.Branch() if len(hook.Owner) == 0 { hook.Owner = data.Repo.Owner.Name } // extract the author and message from the commit // this is kind of experimental, since I don't know // what I'm doing here. if data.Head != nil && data.Head.Author != nil { hook.Message = data.Head.Message hook.Timestamp = data.Head.Timestamp hook.Author = data.Head.Author.Email } else if data.Commits != nil && len(data.Commits) > 0 && data.Commits[0].Author != nil { hook.Message = data.Commits[0].Message hook.Timestamp = data.Commits[0].Timestamp hook.Author = data.Commits[0].Author.Email } return hook, nil }
// Processes a generic POST-RECEIVE hook and // attempts to trigger a build. func Hook(w http.ResponseWriter, r *http.Request) error { // if this is a pull request route // to a different handler if r.Header.Get("X-Github-Event") == "pull_request" { PullRequestHook(w, r) return nil } // get the payload of the message // this should contain a json representation of the // repository and commit details payload := r.FormValue("payload") // parse the github Hook payload hook, err := github.ParseHook([]byte(payload)) if err != nil { println("could not parse hook") return RenderText(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest) } // make sure this is being triggered because of a commit // and not something like a tag deletion or whatever if hook.IsTag() || hook.IsGithubPages() || hook.IsHead() == false || hook.IsDeleted() { return RenderText(w, http.StatusText(http.StatusOK), http.StatusOK) } // get the repo from the URL repoId := r.FormValue("id") // get the repo from the database, return error if not found repo, err := database.GetRepoSlug(repoId) if err != nil { return RenderText(w, http.StatusText(http.StatusNotFound), http.StatusNotFound) } // Get the user that owns the repository user, err := database.GetUser(repo.UserID) if err != nil { return RenderText(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest) } // Verify that the commit doesn't already exist. // We should never build the same commit twice. _, err = database.GetCommitHash(hook.Head.Id, repo.ID) if err != nil && err != sql.ErrNoRows { println("commit already exists") return RenderText(w, http.StatusText(http.StatusBadGateway), http.StatusBadGateway) } // we really only need: // * repo owner // * repo name // * repo host (github) // * commit hash // * commit timestamp // * commit branch // * commit message // * commit author // * pull request // once we have this data we could just send directly to the queue // and let it handle everything else commit := &Commit{} commit.RepoID = repo.ID commit.Branch = hook.Branch() commit.Hash = hook.Head.Id commit.Status = "Pending" commit.Created = time.Now().UTC() // extract the author and message from the commit // this is kind of experimental, since I don't know // what I'm doing here. if hook.Head != nil && hook.Head.Author != nil { commit.Message = hook.Head.Message commit.Timestamp = hook.Head.Timestamp commit.SetAuthor(hook.Head.Author.Email) } else if hook.Commits != nil && len(hook.Commits) > 0 && hook.Commits[0].Author != nil { commit.Message = hook.Commits[0].Message commit.Timestamp = hook.Commits[0].Timestamp commit.SetAuthor(hook.Commits[0].Author.Email) } // get the drone.yml file from GitHub client := github.New(user.GithubToken) content, err := client.Contents.FindRef(repo.Owner, repo.Name, ".drone.yml", commit.Hash) if err != nil { msg := "No .drone.yml was found in this repository. You need to add one.\n" if err := saveFailedBuild(commit, msg); err != nil { return RenderText(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) } return RenderText(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest) } // decode the content. Note: Not sure this will ever happen...it basically means a GitHub API issue raw, err := content.DecodeContent() if err != nil { msg := "Could not decode the yaml from GitHub. Check that your .drone.yml is a valid yaml file.\n" if err := saveFailedBuild(commit, msg); err != nil { return RenderText(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) } return RenderText(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest) } // parse the build script buildscript, err := script.ParseBuild(raw) if err != nil { msg := "Could not parse your .drone.yml file. It needs to be a valid drone yaml file.\n\n" + err.Error() + "\n" if err := saveFailedBuild(commit, msg); err != nil { return RenderText(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) } return RenderText(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest) } // save the commit to the database if err := database.SaveCommit(commit); err != nil { return RenderText(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) } // save the build to the database build := &Build{} build.Slug = "1" // TODO build.CommitID = commit.ID build.Created = time.Now().UTC() build.Status = "Pending" if err := database.SaveBuild(build); err != nil { return RenderText(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) } // notify websocket that a new build is pending //realtime.CommitPending(repo.UserID, repo.TeamID, repo.ID, commit.ID, repo.Private) //realtime.BuildPending(repo.UserID, repo.TeamID, repo.ID, commit.ID, build.ID, repo.Private) queue.Add(&queue.BuildTask{Repo: repo, Commit: commit, Build: build, Script: buildscript}) //Push(repo, commit, build, buildscript) // OK! return RenderText(w, http.StatusText(http.StatusOK), http.StatusOK) }
// HandleMessage reads the nsq message body and parses it as a github webhook, // checks out the source for the repository & builds/uploads the binaries. func (h *Handler) HandleMessage(m *nsq.Message) error { hook, err := github.ParseHook(m.Body) if err != nil { // Errors will most likely occur because not all GH // hooks are the same format // we care about those that are pushes to master logrus.Debugf("Error parsing hook: %v", err) return nil } shortSha := hook.After[0:7] // checkout the code in a temp dir temp, err := ioutil.TempDir("", fmt.Sprintf("commit-%s", shortSha)) if err != nil { return err } defer os.RemoveAll(temp) if err := checkout(temp, hook.Repo.Url, hook.After); err != nil { logrus.Warn(err) return err } logrus.Debugf("Checked out %s for %s", hook.After, hook.Repo.Url) var ( image = fmt.Sprintf("docker:commit-%s", shortSha) container = fmt.Sprintf("build-%s", shortSha) ) logrus.Infof("image=%s container=%s\n", image, container) // build the image if err := build(temp, image); err != nil { logrus.Warn(err) return err } logrus.Debugf("Successfully built image %s", image) // make the binary defer removeContainer(container) if err = makeBinary(temp, image, container, 20*time.Minute); err != nil { logrus.Warn(err) return err } logrus.Debugf("Successfully built binaries for %s", hook.After) // read the version version, err := getBinaryVersion(temp) if err != nil { logrus.Warnf("Getting binary version failed: %v", err) return err } bundlesPath := path.Join(temp, "bundles", version, "cross") // create commit file if err := ioutil.WriteFile(path.Join(bundlesPath, "commit"), []byte(hook.After), 0755); err != nil { return err } // create version file if err := ioutil.WriteFile(path.Join(bundlesPath, "version"), []byte(version), 0755); err != nil { return err } // use env variables to connect to s3 auth, err := aws.EnvAuth() if err != nil { return fmt.Errorf("AWS Auth failed: %v", err) } // connect to s3 bucket s := s3.New(auth, aws.GetRegion(region)) bucketname, bucketpath := bucketParts(bucket) bucket := s.Bucket(bucketname) // push to s3 if err = pushToS3(bucket, bucketpath, bundlesPath); err != nil { logrus.Warn(err) return err } // add html to template if err := createIndexFile(bucket, bucketpath); err != nil { logrus.Warn(err) return err } return nil }